""" Public Routes ============= Public-facing routes: index, company profiles, search, events, new members, connections map, release notes, dashboard. """ import logging from datetime import datetime, timedelta, date from flask import render_template, request, redirect, url_for, flash, session, current_app, Response, jsonify from flask_login import login_required, current_user from sqlalchemy import or_, func from . import bp from database import ( SessionLocal, Company, Category, User, CompanyRecommendation, CompanyEvent, CompanyDigitalMaturity, CompanyWebsiteAnalysis, CompanyQualityTracking, CompanyWebsiteContent, CompanyAIInsights, CompanySocialMedia, CompanyContact, Person, CompanyPerson, GBPAudit, ITAudit, CompanyPKD, NordaEvent, EventAttendee, EventGuest, AIChatConversation, AIChatMessage, UserSession, SearchQuery, MembershipApplication, Announcement, ForumTopic, Classified, UserNotification, UserCompany, SystemRole, ) from utils.helpers import sanitize_input from extensions import limiter from search_service import search_companies # Logger logger = logging.getLogger(__name__) # Global constant (same as in app.py) COMPANY_COUNT_MARKETING = 150 @bp.route('/') def index(): """Homepage - landing page for guests, company directory for logged in users""" if not current_user.is_authenticated: # Landing page for guests with public company tiles db = SessionLocal() try: companies = db.query(Company).filter_by(status='active').order_by(Company.name).all() total_companies = len(companies) total_categories = db.query(Category).count() # Load social media for all companies in one query company_social = {} if companies: social_records = db.query(CompanySocialMedia).filter( CompanySocialMedia.company_id.in_([c.id for c in companies]) ).all() for sm in social_records: company_social.setdefault(sm.company_id, []).append(sm) return render_template( 'landing.html', companies=companies, company_social=company_social, total_companies=total_companies, total_categories=total_categories ) finally: db.close() # Company directory for logged in users db = SessionLocal() try: from datetime import date companies = db.query(Company).filter_by(status='active').order_by(Company.name).all() # Build company relationship maps (parent-child) company_map = {c.id: c for c in companies} company_children = {} # parent_id -> [child names] company_parent = {} # child_id -> parent name for c in companies: if c.parent_company_id: parent = company_map.get(c.parent_company_id) if parent: company_parent[c.id] = parent.name company_children.setdefault(c.parent_company_id, []).append(c.name) else: # Parent may be inactive — look it up inactive_parent = db.query(Company).filter_by(id=c.parent_company_id).first() if inactive_parent: company_parent[c.id] = inactive_parent.name # Get hierarchical categories (main categories with subcategories) main_categories = db.query(Category).filter( Category.parent_id.is_(None) ).order_by(Category.display_order, Category.name).all() # All categories for backwards compatibility categories = db.query(Category).order_by(Category.sort_order).all() 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 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() upcoming_events = [] for event in all_upcoming: if event.can_user_view(current_user): 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) >= 2: break # Backward compat — next_event used by other parts next_event = upcoming_events[0]['event'] if upcoming_events else None # ZOPK Knowledge facts — widget dla zalogowanych (najnowsze z różnych źródeł) zopk_facts = [] if current_user.is_authenticated: try: from database import ZOPKKnowledgeFact, ZOPKNews # Pobierz najnowszy fakt z każdego z 3 różnych artykułów recent_news_ids = db.query( ZOPKKnowledgeFact.source_news_id ).join(ZOPKNews).filter( ZOPKKnowledgeFact.confidence_score >= 0.5, ZOPKNews.published_at.isnot(None) ).group_by( ZOPKKnowledgeFact.source_news_id, ZOPKNews.published_at ).order_by(ZOPKNews.published_at.desc()).limit(3).all() news_ids = [r[0] for r in recent_news_ids] if news_ids: for nid in news_ids: fact = db.query(ZOPKKnowledgeFact).filter( ZOPKKnowledgeFact.source_news_id == nid ).first() if fact: zopk_facts.append(fact) except Exception: db.rollback() # Sprawdź czy użytkownik ma deklarację członkowską w toku pending_application = None if not current_user.is_norda_member and not current_user.company_id: pending_application = db.query(MembershipApplication).filter( MembershipApplication.user_id == current_user.id, MembershipApplication.status.in_(['draft', 'submitted', 'under_review', 'pending_user_approval', 'changes_requested']) ).first() # Latest release for "Co nowego" banner all_releases = _get_releases() latest_release = all_releases[0] if all_releases else None # Newest registered users for homepage newest_users = [] try: newest_users = db.query(User).filter( User.is_active == True, User.is_verified == True, User.name != None ).order_by(User.created_at.desc()).limit(4).all() except Exception: pass # Latest forum topics (2 different topics with recent activity) latest_forum_topics = [] try: from database import ForumTopic, ForumReply from sqlalchemy import func # Get 2 most recently active topics (by latest reply or creation date) topics_with_activity = db.query( ForumTopic, func.coalesce(func.max(ForumReply.created_at), ForumTopic.created_at).label('last_activity') ).outerjoin( ForumReply, (ForumReply.topic_id == ForumTopic.id) & (ForumReply.is_deleted == False) ).filter( ForumTopic.is_deleted == False ).group_by(ForumTopic.id).order_by( func.coalesce(func.max(ForumReply.created_at), ForumTopic.created_at).desc() ).limit(2).all() for t in topics_with_activity: topic = t[0] last_activity = t[1] # Find the author of the latest reply (or topic author if no replies) latest_reply = db.query(ForumReply).filter( ForumReply.topic_id == topic.id, ForumReply.is_deleted == False ).order_by(ForumReply.created_at.desc()).first() if latest_reply: last_author = latest_reply.author else: last_author = topic.author latest_forum_topics.append({ 'topic': topic, 'last_activity': last_activity, 'last_author': last_author }) except Exception: pass # Latest B2B classifieds (2 newest active) latest_classifieds = [] try: from database import Classified from datetime import datetime as dt latest_classifieds = db.query(Classified).filter( Classified.is_active == True, Classified.is_test == False, (Classified.expires_at == None) | (Classified.expires_at > dt.now()) ).order_by(Classified.created_at.desc()).limit(2).all() except Exception: pass # New members — find newest board meeting that has admitted companies latest_admitted = [] last_meeting = None try: from database import BoardMeeting from sqlalchemy import exists meetings_with_admissions = db.query(BoardMeeting).filter( exists().where(Company.admitted_at_meeting_id == BoardMeeting.id) ).order_by(BoardMeeting.meeting_date.desc()).first() if meetings_with_admissions: last_meeting = meetings_with_admissions 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, categories=categories, main_categories=main_categories, total_companies=total_companies, total_categories=total_categories, next_event=next_event, upcoming_events=upcoming_events, pending_application=pending_application, zopk_facts=zopk_facts, latest_release=latest_release, company_children=company_children, company_parent=company_parent, newest_users=newest_users, latest_admitted=latest_admitted, last_meeting=last_meeting, latest_forum_topics=latest_forum_topics, latest_classifieds=latest_classifieds ) finally: db.close() @bp.route('/api/zopk-facts') @login_required def api_zopk_facts(): """API endpoint for loading more ZOPK facts (for homepage widget).""" from database import ZOPKKnowledgeFact, ZOPKNews offset = request.args.get('offset', 0, type=int) db = SessionLocal() try: recent_news_ids = db.query( ZOPKKnowledgeFact.source_news_id ).join(ZOPKNews).filter( ZOPKKnowledgeFact.confidence_score >= 0.5, ZOPKNews.published_at.isnot(None) ).group_by( ZOPKKnowledgeFact.source_news_id, ZOPKNews.published_at ).order_by(ZOPKNews.published_at.desc()).offset(offset).limit(3).all() news_ids = [r[0] for r in recent_news_ids] facts = [] for nid in news_ids: fact = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter( ZOPKKnowledgeFact.source_news_id == nid ).first() if fact: type_labels = {'investment': 'inwestycja', 'decision': 'decyzja', 'event': 'wydarzenie', 'milestone': 'kamień milowy', 'statistic': 'dane', 'partnership': 'współpraca', 'project': 'projekt'} facts.append({ 'text': fact.full_text[:200], 'type': fact.fact_type, 'type_label': type_labels.get(fact.fact_type, 'fakt'), 'source_name': fact.source_news.source_name or fact.source_news.source_domain if fact.source_news else '', 'source_date': fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news and fact.source_news.published_at else '', 'source_url': fact.source_news.url if fact.source_news else '', }) return jsonify({'facts': facts, 'has_more': len(facts) == 3}) except Exception: db.rollback() return jsonify({'facts': [], 'has_more': False}) finally: db.close() @bp.route('/api/upcoming-events') @login_required def api_upcoming_events(): """API: Najbliższe wydarzenia z filtrem typu (all/norda/external)""" from database import NordaEvent, EventAttendee event_filter = request.args.get('filter', 'all') limit = min(request.args.get('limit', 6, type=int), 12) db = SessionLocal() try: query = db.query(NordaEvent).filter(NordaEvent.event_date >= date.today()) if event_filter == 'norda': query = query.filter(NordaEvent.is_external == False) elif event_filter == 'external': query = query.filter(NordaEvent.is_external == True) all_events = query.order_by(NordaEvent.event_date.asc()).all() results = [] for event in all_events: if not event.can_user_view(current_user): continue 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) days = ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'] results.append({ 'id': event.id, 'title': event.title, 'date': event.event_date.strftime('%d.%m.%Y'), 'day': days[event.event_date.weekday()], 'time': event.time_start.strftime('%H:%M') if event.time_start else None, 'location': (event.location[:30] + '...' if event.location and len(event.location) > 30 else event.location) if event.location else None, 'attendee_count': event.total_attendee_count, 'is_external': event.is_external, 'external_source': event.external_source, 'access_level': event.access_level, 'user_registered': registered, 'user_can_attend': can_attend, 'url': url_for('calendar.calendar_event', event_id=event.id), }) if len(results) >= limit: break return jsonify({'events': results}) finally: db.close() @bp.route('/izba/wladze') @login_required def chamber_authorities(): """Władze Izby — Zarząd, Rada, Komisja Rewizyjna, Sąd Koleżeński""" from utils.decorators import member_required db = SessionLocal() try: # Group users by chamber role all_with_roles = db.query(User).filter( User.chamber_role.isnot(None), User.is_active == True ).order_by(User.name).all() groups = { 'zarzad': {'title': 'Zarząd Izby', 'icon': '👔', 'members': []}, 'rada': {'title': 'Rada Izby', 'icon': '🏛️', 'members': []}, 'komisja': {'title': 'Komisja Rewizyjna', 'icon': '📋', 'members': []}, 'sad': {'title': 'Sąd Koleżeński', 'icon': '⚖️', 'members': []}, } for u in all_with_roles: if u.chamber_role in ('prezes', 'wiceprezes'): groups['zarzad']['members'].append(u) elif u.chamber_role == 'czlonek_rady': groups['rada']['members'].append(u) elif u.chamber_role == 'komisja_rewizyjna': groups['komisja']['members'].append(u) elif u.chamber_role == 'sad_kolezenski': groups['sad']['members'].append(u) # Sort zarząd: prezes first, then wiceprezesi groups['zarzad']['members'].sort(key=lambda u: (0 if u.chamber_role == 'prezes' else 1, u.name)) return render_template('chamber_authorities.html', groups=groups) finally: db.close() @bp.route('/company/') def company_detail(company_id): """Company detail page - requires login and NORDA membership""" # Sprawdź czy użytkownik jest zalogowany if not current_user.is_authenticated: flash('Zaloguj się, aby zobaczyć szczegóły firmy.', 'warning') return redirect(url_for('auth.login')) # Guest flag — logged in but not a member (sees limited company profile) is_guest = not current_user.is_norda_member and not current_user.company_id and not current_user.has_role(SystemRole.MEMBER) db = SessionLocal() try: company = db.query(Company).filter_by(id=company_id).first() if not company: flash('Firma nie znaleziona.', 'error') return redirect(url_for('index')) # Load digital maturity data if available maturity_data = db.query(CompanyDigitalMaturity).filter_by(company_id=company_id).first() # Get all website analyses sorted by audit date (latest first) all_analyses = db.query(CompanyWebsiteAnalysis).filter_by( company_id=company_id ).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).all() # Build dict: company_website_id → analysis (latest first wins) analyses_by_website = {} website_analysis = None # backward compat: keep "main" analysis for a in all_analyses: if a.company_website_id and a.company_website_id not in analyses_by_website: analyses_by_website[a.company_website_id] = a if website_analysis is None: website_analysis = a # first = latest = main analysis # Load quality tracking data quality_data = db.query(CompanyQualityTracking).filter_by(company_id=company_id).first() # Load company events (latest 10) events = db.query(CompanyEvent).filter_by(company_id=company_id).order_by( CompanyEvent.event_date.desc(), CompanyEvent.created_at.desc() ).limit(10).all() # Load website scraping data (most recent) website_content = db.query(CompanyWebsiteContent).filter_by(company_id=company_id).order_by( CompanyWebsiteContent.scraped_at.desc() ).first() # Load AI insights ai_insights = db.query(CompanyAIInsights).filter_by(company_id=company_id).first() # Load social media profiles social_media = db.query(CompanySocialMedia).filter_by(company_id=company_id).all() # Load company contacts (phones, emails with sources) contacts = db.query(CompanyContact).filter_by(company_id=company_id).order_by( CompanyContact.contact_type, CompanyContact.is_primary.desc() ).all() # Load recommendations (approved only, with recommender details) recommendations = db.query(CompanyRecommendation).filter_by( company_id=company_id, status='approved' ).join(User, CompanyRecommendation.user_id == User.id).order_by( CompanyRecommendation.created_at.desc() ).all() # Load people connected to company (zarząd, wspólnicy, prokurenci) people = db.query(CompanyPerson).filter_by( company_id=company_id ).join(Person, CompanyPerson.person_id == Person.id).order_by( CompanyPerson.role_category, Person.nazwisko ).all() # Load portal users linked to this company (for "Osoby kontaktowe" section) company_users = db.query(UserCompany).filter_by( company_id=company_id ).join(User, UserCompany.user_id == User.id).filter( User.is_active == True ).order_by( UserCompany.role.desc(), # MANAGER first, then EMPLOYEE User.name ).all() # Eager-load user attributes before session close for uc in company_users: _ = uc.user.name, uc.user.email, uc.user.phone, uc.user.is_norda_member _ = uc.user.privacy_show_phone, uc.user.privacy_show_email _ = uc.user.contact_prefer_email, uc.user.contact_prefer_phone _ = uc.user.contact_prefer_portal, uc.user.contact_note # Load GBP audit (most recent) gbp_audit = db.query(GBPAudit).filter_by( company_id=company_id ).order_by(GBPAudit.audit_date.desc()).first() # Load IT audit (most recent) it_audit = db.query(ITAudit).filter_by( company_id=company_id ).order_by(ITAudit.audit_date.desc()).first() # Load PKD codes (all - primary first) pkd_codes = db.query(CompanyPKD).filter_by( company_id=company_id ).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all() # Check if current user can enrich company data (user with company edit rights) can_enrich = False can_edit_profile = False company_managers = [] if current_user.is_authenticated: can_enrich = current_user.can_edit_company(company.id) can_edit_profile = current_user.can_manage_company(company.id) # If user is a member but not manager, load managers for contact modal if not can_edit_profile: is_company_member = any( assoc.company_id == company.id for assoc in (current_user.company_associations or []) ) or current_user.company_id == company.id if is_company_member: company_managers = db.query(User).join(UserCompany).filter( UserCompany.company_id == company.id, UserCompany.role == 'MANAGER' ).all() for m in company_managers: _ = m.name, m.email # force-load before session close # ZOPK project links — powiązania z projektami ZOPK zopk_links = [] try: from database import ZOPKCompanyLink, ZOPKProject zopk_links = db.query(ZOPKCompanyLink).join(ZOPKProject).filter( ZOPKCompanyLink.company_id == company_id, ZOPKCompanyLink.relevance_score >= 40 ).order_by(ZOPKCompanyLink.relevance_score.desc()).all() # Eager-load project data for link in zopk_links: _ = link.project.name, link.project.slug, link.project.project_type except Exception: pass # Profile completeness for AI enrichment button profile_fields = { 'Opis firmy': bool(ai_insights and ai_insights.business_summary) or bool(company.description_full), 'Usługi': bool(ai_insights and ai_insights.services_list) or bool(company.services_offered), 'Grupa docelowa': bool(ai_insights and ai_insights.target_market), 'Wyróżniki firmy': bool(ai_insights and ai_insights.unique_selling_points), 'Wartości firmy': bool(ai_insights and ai_insights.company_values), 'Tagi branżowe': bool(ai_insights and ai_insights.industry_tags), } profile_filled = sum(1 for v in profile_fields.values() if v) profile_total = len(profile_fields) profile_missing = [k for k, v in profile_fields.items() if not v] # For child brands — inherit NIP from parent for display/enrichment effective_nip = company.nip if not effective_nip and company.parent_company_id: parent = db.query(Company).filter_by(id=company.parent_company_id).first() if parent: effective_nip = parent.nip return render_template('company_detail.html', company=company, company_id=company.id, # For analytics conversion tracking effective_nip=effective_nip, maturity_data=maturity_data, website_analysis=website_analysis, analyses_by_website=analyses_by_website, quality_data=quality_data, events=events, website_content=website_content, ai_insights=ai_insights, social_media=social_media, contacts=contacts, recommendations=recommendations, people=people, company_users=company_users, gbp_audit=gbp_audit, it_audit=it_audit, pkd_codes=pkd_codes, can_enrich=can_enrich, can_edit_profile=can_edit_profile, profile_filled=profile_filled, profile_total=profile_total, profile_missing=profile_missing, company_managers=company_managers, is_admin=current_user.is_authenticated and current_user.is_admin, zopk_links=zopk_links, is_guest=is_guest, ) finally: db.close() @bp.route('/company/') def company_detail_by_slug(slug): """Company detail page by slug - requires login""" db = SessionLocal() try: company = db.query(Company).filter_by(slug=slug).first() if not company: flash('Firma nie znaleziona.', 'error') return redirect(url_for('index')) # Redirect to canonical int ID route return redirect(url_for('company_detail', company_id=company.id)) finally: db.close() @bp.route('/osoba/') @login_required def person_detail(person_id): """Person detail page - shows registry data and portal data if available""" if not current_user.is_norda_member and not current_user.has_role(SystemRole.MEMBER): flash('Profile osób są dostępne tylko dla członków Izby NORDA.', 'info') return redirect(url_for('membership.apply')) db = SessionLocal() try: # Get person with their company relationships person = db.query(Person).filter_by(id=person_id).first() if not person: flash('Osoba nie znaleziona.', 'error') return redirect(url_for('index')) # Get company roles with company details (only active companies) company_roles = db.query(CompanyPerson).filter_by( person_id=person_id ).join(Company, CompanyPerson.company_id == Company.id).filter( Company.status == 'active' ).order_by( CompanyPerson.role_category, Company.name ).all() # Find matching user account - prefer direct person_id link, then name match portal_user = db.query(User).filter( User.person_id == person_id ).first() if not portal_user: name_parts = person.full_name().upper().split() if len(name_parts) >= 2: candidates = [] for u in db.query(User).filter(User.name.isnot(None)).all(): if u.name: user_name_parts = u.name.upper().split() if len(user_name_parts) >= 2: if (user_name_parts[-1] in name_parts and any(part in user_name_parts for part in name_parts[:-1])): candidates.append(u) if candidates: # Prefer user with person_id set, then non-gmail accounts candidates.sort(key=lambda u: ( 0 if u.person_id else 1, 0 if not u.email.endswith('@gmail.com') else 1, u.id, )) 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', person=person, company_roles=company_roles, 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: db.close() @bp.route('/profil/') @login_required def user_profile(user_id): """User profile page - redirects to person_detail if person_id exists, otherwise shows a simple profile from User data.""" if not current_user.is_norda_member and not current_user.has_role(SystemRole.MEMBER): flash('Profile osób są dostępne tylko dla członków Izby NORDA.', 'info') return redirect(url_for('membership.apply')) db = SessionLocal() try: user = db.query(User).filter_by(id=user_id).first() if not user: flash('Użytkownik nie znaleziony.', 'error') return redirect(url_for('index')) # If user has person_id, redirect to full person profile if user.person_id: return redirect(url_for('person_detail', person_id=user.person_id)) # Build simple profile from User data from datetime import datetime, timedelta from database import EventAttendee, NordaEvent, ForumTopic, ForumReply, UserCompany, CompanyRole # Last activity label last_active_label = None if user.last_login: diff = datetime.now() - 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): last_active_label = f'Aktywny {diff.days} dni temu' elif diff < timedelta(days=60): last_active_label = f'Aktywny {diff.days // 7} tyg. temu' else: last_active_label = f'Ostatnio: {user.last_login.strftime("%d.%m.%Y")}' # Company associations user_companies = db.query(UserCompany).filter_by(user_id=user.id).all() # Events attended attended_events = db.query(NordaEvent).join( EventAttendee, EventAttendee.event_id == NordaEvent.id ).filter( EventAttendee.user_id == 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=user.id, is_deleted=False).count() forum_replies_count = db.query(ForumReply).filter_by( author_id=user.id, is_deleted=False).count() return render_template('user_profile.html', profile_user=user, last_active_label=last_active_label, user_companies=user_companies, attended_events=attended_events, forum_topics_count=forum_topics_count, forum_replies_count=forum_replies_count, ) finally: db.close() @bp.route('/company//recommend', methods=['GET', 'POST']) def company_recommend(slug): """Create recommendation for a company - requires login""" db = SessionLocal() try: # Get company company = db.query(Company).filter_by(slug=slug).first() if not company: flash('Firma nie znaleziona.', 'error') return redirect(url_for('index')) # Handle POST (form submission) if request.method == 'POST': recommendation_text = request.form.get('recommendation_text', '').strip() service_category = sanitize_input(request.form.get('service_category', ''), 200) show_contact = request.form.get('show_contact') == '1' # Validation if not recommendation_text or len(recommendation_text) < 50: flash('Rekomendacja musi mieć co najmniej 50 znaków.', 'error') return render_template('company/recommend.html', company=company) if len(recommendation_text) > 2000: flash('Rekomendacja może mieć maksymalnie 2000 znaków.', 'error') return render_template('company/recommend.html', company=company) # Prevent self-recommendation if current_user.company_id == company.id: flash('Nie możesz polecać własnej firmy.', 'error') return redirect(url_for('company_detail', company_id=company.id)) # Check for duplicate (user already recommended this company) existing = db.query(CompanyRecommendation).filter_by( user_id=current_user.id, company_id=company.id ).first() if existing: flash('Już poleciłeś tę firmę. Możesz edytować swoją wcześniejszą rekomendację.', 'error') return redirect(url_for('company_detail', company_id=company.id)) # Create recommendation recommendation = CompanyRecommendation( company_id=company.id, user_id=current_user.id, recommendation_text=recommendation_text, service_category=service_category if service_category else None, show_contact=show_contact, status='pending' ) db.add(recommendation) db.commit() flash('Dziękujemy! Twoja rekomendacja została przesłana i oczekuje na moderację.', 'success') return redirect(url_for('company_detail', company_id=company.id)) # Handle GET (show form) return render_template('company/recommend.html', company=company) finally: db.close() @bp.route('/search') @login_required def search(): """Search companies and people with advanced matching - requires login""" query = request.args.get('q', '') category_id = request.args.get('category', type=int) db = SessionLocal() try: # Use new SearchService with synonym expansion, NIP/REGON lookup, and fuzzy matching results = search_companies(db, query, category_id, limit=50) # Extract companies from SearchResult objects companies = [r.company for r in results] # Log search to analytics (SearchQuery table) search_query_id = None if query: try: analytics_session_id = session.get('analytics_session_id') session_db_id = None if analytics_session_id: user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first() if user_session: session_db_id = user_session.id search_query = SearchQuery( session_id=session_db_id, user_id=current_user.id if current_user.is_authenticated else None, query=query[:500], query_normalized=query.lower().strip()[:500], results_count=len(companies), has_results=len(companies) > 0, search_type='main', filters_used={'category_id': category_id} if category_id else None ) db.add(search_query) db.commit() search_query_id = search_query.id except Exception as e: logger.error(f"Search logging error: {e}") db.rollback() search_query_id = None # For debugging/analytics - log search stats if query: match_types = {} for r in results: match_types[r.match_type] = match_types.get(r.match_type, 0) + 1 logger.info(f"Search '{query}': {len(companies)} results, types: {match_types}") # Search people by name (partial match) people_results = [] if query and len(query) >= 2: q = f"%{query}%" people_results = db.query(Person).filter( or_( Person.imiona.ilike(q), Person.nazwisko.ilike(q), func.concat(Person.imiona, ' ', Person.nazwisko).ilike(q) ) ).limit(20).all() # For each person, get their company connections count for person in people_results: person.company_count = len(set( r.company_id for r in person.company_roles if r.company and r.company.status == 'active' )) logger.info(f"Search '{query}': {len(people_results)} people found") # Also search portal users (not yet in people_results via person_id) user_results = [] if query and len(query) >= 2: q = f"%{query}%" matched_users = db.query(User).filter( User.is_active == True, User.is_verified == True, User.name.ilike(q) ).limit(20).all() # Exclude users who are already in people_results (via person_id) people_person_ids = {p.id for p in people_results} for u in matched_users: if u.person_id and u.person_id in people_person_ids: continue # already shown as Person user_results.append(u) if user_results: logger.info(f"Search '{query}': {len(user_results)} portal users found") return render_template( 'search_results.html', companies=companies, people=people_results, user_results=user_results, query=query, category_id=category_id, result_count=len(companies), search_query_id=search_query_id ) finally: db.close() @bp.route('/aktualnosci') @login_required def events(): """DEPRECATED (2026-03-12): Sekcja nieużywana, zastąpiona przez /ogloszenia (Announcements). Route zachowany dla kompatybilności wstecznej - nie rozwijać.""" event_type_filter = request.args.get('type', '') company_id = request.args.get('company', type=int) page = request.args.get('page', 1, type=int) per_page = 20 db = SessionLocal() try: # Build query query = db.query(CompanyEvent).join(Company) # Apply filters if event_type_filter: query = query.filter(CompanyEvent.event_type == event_type_filter) if company_id: query = query.filter(CompanyEvent.company_id == company_id) # Order by date (newest first) query = query.order_by( CompanyEvent.event_date.desc(), CompanyEvent.created_at.desc() ) # Pagination total_events = query.count() events = query.limit(per_page).offset((page - 1) * per_page).all() # Get companies with events for filter dropdown companies_with_events = db.query(Company).join(CompanyEvent).distinct().order_by(Company.name).all() # Event type statistics event_types = db.query( CompanyEvent.event_type, func.count(CompanyEvent.id) ).group_by(CompanyEvent.event_type).all() return render_template( 'events.html', events=events, companies_with_events=companies_with_events, event_types=event_types, event_type_filter=event_type_filter, company_id=company_id, page=page, per_page=per_page, total_events=total_events, total_pages=(total_events + per_page - 1) // per_page ) finally: db.close() @bp.route('/nowi-czlonkowie') @login_required def new_members(): """Lista nowych członków Izby — pogrupowana wg posiedzeń Rady.""" db = SessionLocal() try: from database import BoardMeeting from sqlalchemy import exists # Get meetings that have admitted companies, newest first meetings = db.query(BoardMeeting).filter( exists().where(Company.admitted_at_meeting_id == BoardMeeting.id) ).order_by(BoardMeeting.meeting_date.desc()).limit(12).all() # For each meeting, get admitted companies meetings_data = [] total = 0 for meeting in meetings: companies = db.query(Company).filter( Company.admitted_at_meeting_id == meeting.id ).order_by(Company.name).all() meetings_data.append({ 'meeting': meeting, 'companies': companies }) total += len(companies) return render_template('new_members.html', meetings_data=meetings_data, total=total ) finally: db.close() @bp.route('/mapa-polaczen') def connections_map(): """Company-person connections visualization page""" return render_template('connections_map.html') @bp.route('/dashboard') @login_required def dashboard(): """User dashboard""" db = SessionLocal() try: # Get user's conversations conversations = db.query(AIChatConversation).filter_by( user_id=current_user.id ).order_by(AIChatConversation.updated_at.desc()).limit(10).all() # Stats total_conversations = db.query(AIChatConversation).filter_by(user_id=current_user.id).count() total_messages = db.query(AIChatMessage).join(AIChatConversation).filter( AIChatConversation.user_id == current_user.id ).count() # New stats unread_notifications = db.query(UserNotification).filter( UserNotification.user_id == current_user.id, UserNotification.is_read == False ).count() upcoming_events_count = db.query(NordaEvent).filter( NordaEvent.event_date >= date.today() ).count() user_forum_topics_count = db.query(ForumTopic).filter( ForumTopic.author_id == current_user.id, ForumTopic.is_deleted == False ).count() # Check for membership application status has_pending_application = False has_draft_application = False pending_application = None try: from database import MembershipApplication pending_application = db.query(MembershipApplication).filter( MembershipApplication.user_id == current_user.id, MembershipApplication.status.in_(['submitted', 'under_review', 'changes_requested']) ).first() has_pending_application = pending_application is not None if not has_pending_application: draft = db.query(MembershipApplication).filter( MembershipApplication.user_id == current_user.id, MembershipApplication.status == 'draft' ).first() has_draft_application = draft is not None except Exception: pass # MembershipApplication table may not exist yet # Load user's company associations (multi-company support) from sqlalchemy.orm import joinedload user_companies = db.query(UserCompany).options( joinedload(UserCompany.company) ).filter_by( user_id=current_user.id ).order_by(UserCompany.is_primary.desc(), UserCompany.created_at.asc()).all() # Force-load company names before session closes for uc in user_companies: _ = uc.company.name if uc.company else None # Managers map for companies where user is EMPLOYEE (for edit permission modal) company_managers_map = {} employee_company_ids = [uc.company_id for uc in user_companies if uc.role == 'EMPLOYEE'] if employee_company_ids: managers = db.query(User, UserCompany.company_id).join(UserCompany).filter( UserCompany.company_id.in_(employee_company_ids), UserCompany.role == 'MANAGER' ).all() for mgr, cid in managers: company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''}) # Onboarding progress widget from utils.onboarding import compute_onboarding_steps onboarding = compute_onboarding_steps(current_user, user_companies, current_app.root_path) # Widget 1: Upcoming events (3 nearest future events) upcoming_events = db.query(NordaEvent).filter( NordaEvent.event_date >= date.today() ).order_by(NordaEvent.event_date.asc()).limit(3).all() # Batch RSVP lookup for current user + attendee counts user_event_ids = set() event_attendee_counts = {} if upcoming_events: event_ids = [e.id for e in upcoming_events] rsvps = db.query(EventAttendee.event_id).filter( EventAttendee.event_id.in_(event_ids), EventAttendee.user_id == current_user.id, EventAttendee.status == 'confirmed' ).all() user_event_ids = {r[0] for r in rsvps} # Count confirmed attendees + guests per event from sqlalchemy import func counts = db.query(EventAttendee.event_id, func.count(EventAttendee.id)).filter( EventAttendee.event_id.in_(event_ids), EventAttendee.status == 'confirmed' ).group_by(EventAttendee.event_id).all() event_attendee_counts = {eid: cnt for eid, cnt in counts} guest_counts = db.query(EventGuest.event_id, func.count(EventGuest.id)).filter( EventGuest.event_id.in_(event_ids) ).group_by(EventGuest.event_id).all() for eid, cnt in guest_counts: event_attendee_counts[eid] = event_attendee_counts.get(eid, 0) + cnt # Widget 2: Recent announcements (3 latest published, pinned first, not expired) recent_announcements = db.query(Announcement).filter( Announcement.status == 'published', or_(Announcement.expires_at == None, Announcement.expires_at > datetime.now()) ).order_by(Announcement.is_pinned.desc(), Announcement.published_at.desc()).limit(3).all() # Widget 3: Recent forum topics (5 latest active) recent_forum_topics = db.query(ForumTopic).filter( ForumTopic.is_deleted == False ).order_by(ForumTopic.updated_at.desc()).limit(5).all() # Widget 4: Recent classifieds (3 active, not test, not expired) recent_classifieds = db.query(Classified).filter( Classified.is_active == True, Classified.is_test == False, or_(Classified.expires_at == None, Classified.expires_at > datetime.now()) ).order_by(Classified.created_at.desc()).limit(3).all() # Widget 5: New companies (3 newest) new_companies = db.query(Company).order_by( Company.created_at.desc() ).limit(3).all() # Admin KPI widget (only computed for admins) admin_kpi = None if current_user.can_access_admin_panel(): from sqlalchemy import func as sqlfunc from database import UserSession, PageView, SecurityAlert from datetime import timedelta as td now = datetime.now() week_ago = now - td(days=7) prev_week = now - td(days=14) # Active members (logged in last 7d) active_7d = db.query(sqlfunc.count(sqlfunc.distinct(UserSession.user_id))).filter( UserSession.started_at >= week_ago, UserSession.user_id.isnot(None), UserSession.is_bot == False ).scalar() or 0 # Total registered users total_users = db.query(sqlfunc.count(User.id)).filter(User.is_active == True).scalar() or 0 # Sessions this week sessions_7d = db.query(sqlfunc.count(UserSession.id)).filter( UserSession.started_at >= week_ago, UserSession.is_bot == False ).scalar() or 0 # Active problems (security alerts + locked accounts) active_problems = db.query(sqlfunc.count(SecurityAlert.id)).filter( SecurityAlert.created_at >= week_ago ).scalar() or 0 # Never logged in never_logged = db.query(sqlfunc.count(User.id)).filter( User.is_active == True, User.last_login.is_(None) ).scalar() or 0 admin_kpi = { 'active_users': active_7d, 'total_users': total_users, 'active_pct': round(active_7d / total_users * 100) if total_users > 0 else 0, 'sessions': sessions_7d, 'problems': active_problems, 'never_logged': never_logged, } return render_template( 'dashboard.html', admin_kpi=admin_kpi, conversations=conversations, total_conversations=total_conversations, total_messages=total_messages, has_pending_application=has_pending_application, has_draft_application=has_draft_application, pending_application=pending_application, user_companies=user_companies, unread_notifications=unread_notifications, upcoming_events_count=upcoming_events_count, user_forum_topics_count=user_forum_topics_count, upcoming_events=upcoming_events, user_event_ids=user_event_ids, event_attendee_counts=event_attendee_counts, recent_announcements=recent_announcements, recent_forum_topics=recent_forum_topics, recent_classifieds=recent_classifieds, new_companies=new_companies, company_managers_map=company_managers_map, onboarding=onboarding ) finally: db.close() def _get_releases(): """Return list of all release notes entries.""" return [ { 'version': 'v1.68.0', 'date': '14 kwietnia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Ustawienia powiadomień': '/konto/prywatnosc', }, 'new': [ '★ Powiadomienia na urządzeniu (push) - kliknij ikonę telefonu z falami w górnym pasku i zezwól w przeglądarce, a zobaczysz natychmiastowe powiadomienia o nowych wiadomościach i aktywności, nawet gdy portal jest zamknięty. Działa na komputerze (Chrome, Firefox, Edge), Androidzie i iPhone (po dodaniu strony do ekranu początkowego). Trzeba to włączyć osobno na każdym urządzeniu.', '★ Powiadomienia o kilkunastu typach zdarzeń - dostaniesz powiadomienie gdy ktoś napisze prywatną wiadomość, wyrazi zainteresowanie Twoim ogłoszeniem B2B lub zada do niego pytanie, odpowie w Twoim wątku na forum, zacytuje Twój wpis, a także o nowych aktualnościach Izby, posiedzeniach Rady (utworzenie, program, protokół), nowych wydarzeniach w kalendarzu i przypomnieniu 24 godziny przed wydarzeniem na które jesteś zapisany.', 'Panel preferencji powiadomień - w „Moje konto" → „Prywatność" znajdziesz dwie karty: „Powiadomienia e-mail" i „Powiadomienia push". Dla każdego kanału i każdego typu zdarzenia niezależnie zdecydujesz, co chcesz otrzymywać. Domyślnie powiadomienia masowe (aktualności, Rada) przychodzą tylko na urządzenie a e-maile są wyłączone, żeby nie zapychać skrzynki — możesz włączyć co uznasz.', 'Szybkie wyłączenie powiadomień e-mail - w każdym e-mailu z powiadomieniem pojawia się link, którym jednym kliknięciem wyłączysz dany typ bez logowania do portalu. Gmail i Apple Mail pokażą też swój wbudowany przycisk „Wypisz się" obok adresu nadawcy.', 'Przypomnienie 24h przed wydarzeniem - jeśli jesteś zapisany na wydarzenie z kalendarza Izby, dzień przed otrzymasz automatyczne przypomnienie (push + mail), żebyś nie zapomniał.', ], 'improve': [ 'Nowa ikona powiadomień push - w nagłówku pojawia się ikona smartfona z falami dźwiękowymi, wyraźnie odróżniająca powiadomienia na urządzeniu od powiadomień portalowych (dzwoneczek obok).', 'Klik w wątek forum otwiera najnowszy wpis - wchodząc w temat z listy forum, dashboardu lub sekcji „Nowe na portalu" od razu zobaczysz ostatnią odpowiedź, zamiast scrollować od początku.', ], 'fix': [ 'Zabezpieczenie przed duplikatami ogłoszeń - naprawiono sytuację, w której szybkie kilkukrotne kliknięcie przycisku „Dodaj ogłoszenie" mogło utworzyć kilka identycznych wpisów; analogiczna ochrona dołożona do formularza aktualności i tworzenia posiedzeń Rady Izby.', ], }, { 'version': 'v1.67.0', 'date': '13 kwietnia 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Oznaczanie osób w forum (@wzmianka) - wpisując @ w polu wiadomości dostajesz podpowiedzi z listą użytkowników i możesz jednym kliknięciem oznaczyć konkretną osobę, która od razu dostanie powiadomienie e-mail o wzmiance.', ], 'improve': [ 'Wyróżnienie wzmianki w treści - gdy ktoś oznaczy Ciebie w wątku forum, Twoje imię jest podświetlone kolorowym tłem, żebyś od razu zobaczył, że jesteś wywołany.', ], 'fix': [ 'Zabezpieczenie przed duplikatami wątków forum - szybkie kilkukrotne kliknięcie „Dodaj temat" nie tworzy już kilku identycznych wpisów.', ], }, { 'version': 'v1.66.0', 'date': '12 kwietnia 2026', 'badges': ['fix'], 'fix': [ 'Strona szczegółów ogłoszenia - naprawiono błąd, który powodował, że kliknięcie niektórych ogłoszeń (m.in. konferencji „Odporność to przewaga konkurencyjna") pokazywało stronę błędu zamiast treści', ], }, { 'version': 'v1.65.0', 'date': '11 kwietnia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Przełącznik firm': '/', 'Ogłoszenia B2B': '/tablica/', }, 'new': [ '★ Przełącznik firm w menu - osoby związane z kilkoma firmami mogą szybko przełączać aktywną firmę z poziomu nagłówka, bez wylogowywania i ponownego logowania', 'Wybór firmy w ogłoszeniach B2B - dodając ogłoszenie, możesz wybrać, z której ze swoich firm chcesz je opublikować', 'Wybór firmy na profilu użytkownika - osoby pracujące w kilku firmach mają na swoim profilu rozwijaną listę firm z możliwością wejścia na profil każdej z nich', ], 'improve': [ 'Klikalne linki w wiadomościach - adresy stron internetowych wklejone w prywatnej rozmowie stają się aktywnymi linkami', 'Lista firm na profilu - firmy są teraz wyświetlane pionowo, w kolejności alfabetycznej, z naprzemiennym tłem dla lepszej czytelności', 'Kalendarz dopasowany do aktywnej firmy - lista znajomych z tej samej firmy pokazywana w kalendarzu zmienia się razem z wybraną aktywną firmą', 'Panel składek dla biura - biuro zarządu widzi pełną analizę składek firm-matek i marek, z oznaczeniem niedopłat i nadpłat oraz kolorowymi kafelkami miesięcy', ], 'fix': [ 'Podwójne wiadomości - ta sama wiadomość nie pojawia się już dwa razy w rozmowie', 'Strona wydarzeń dla nowych użytkowników - osoby bez przypisanej firmy nie widzą już błędu przy otwarciu strony wydarzenia', 'Powroty w wiadomościach - przycisk wstecz w rozmowach wraca na właściwą listę rozmów', 'Licznik nieprzeczytanych - czerwona kropka przy ikonie wiadomości pokazuje teraz rzeczywistą liczbę nieprzeczytanych rozmów', ], }, { 'version': 'v1.64.0', 'date': '10 kwietnia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Klikalne profile': '/forum', 'Widok Karty w kalendarzu': '/kalendarz/?view=cards', 'Nowe na portalu': '/', }, 'new': [ '★ Zdjęcia w ogłoszeniach B2B - do każdego ogłoszenia można dodać do 10 zdjęć z podglądem, przeciąganiem i galerią powiększeń', '★ Edycja ogłoszeń B2B - autor może edytować treść, kategorie i zdjęcia w swoim ogłoszeniu po jego opublikowaniu', 'Widok Karty w kalendarzu - nowy sposób przeglądania wydarzeń w dwóch kolumnach, z filtrowaniem po typie (Norda Biznes / zewnętrzne)', 'Nowe na portalu - na stronie głównej widoczne są najnowsze tematy z forum i ogłoszenia B2B, a także lista nowo zarejestrowanych użytkowników', 'Przedłużanie ogłoszeń - wygasłe ogłoszenie można przedłużyć o 30 dni jednym kliknięciem', ], 'improve': [ 'Klikalne profile - kliknięcie w nazwę lub zdjęcie użytkownika na forum, w ogłoszeniach, aktualności czy Radzie Izby przenosi do jego profilu', 'Zdjęcia w sekcji „Widziane przez" - osoby które przeczytały wpis na forum są wyświetlane ze swoimi zdjęciami profilowymi zamiast samych inicjałów', ], 'fix': [ 'Polskie znaki w ogłoszeniach - poprawiono brakujące polskie litery w formularzach i na liście ogłoszeń B2B', ], }, { 'version': 'v1.63.0', 'date': '9 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Ogłoszenia B2B': '/tablica/', }, 'new': [ '★ Powiadomienia w ogłoszeniach B2B - pytania, odpowiedzi i zainteresowania w ogłoszeniach generują powiadomienia na portalu i w e-mail', 'Kontakt z autorem ogłoszenia - przycisk „Skontaktuj się" otwiera nową rozmowę z gotowym kontekstem, do którego ogłoszenia się odnosisz', ], 'improve': [ 'Zdjęcia profilowe w całym portalu - awatary użytkowników wyświetlają się na stronie Rady Izby, w ogłoszeniach B2B i na liście władz', 'Przycisk wstecz w aplikacji - korzystając z portalu jako aplikacji na telefonie, przycisk „Wstecz" przenosi do poprzedniej strony', 'Godziny w wiadomościach - poprawiono wyświetlanie godzin w rozmowach i wiadomościach', ], }, { 'version': 'v1.62.0', 'date': '8 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Nowa wiadomość do wybranej osoby': '/wiadomosci', 'Tworzenie grup rozmów': '/wiadomosci', 'Zarządzanie członkami grupy': '/wiadomosci', }, 'new': [ '★ Nowa wiadomość do wybranej osoby - przycisk „Nowa wiadomość" otwiera okno z wyszukiwarką osób — wystarczy zacząć pisać imię lub nazwisko, by znaleźć odbiorcę', '★ Tworzenie grup rozmów - przycisk „Nowa grupa" pozwala nadać nazwę, wybrać członków i od razu rozpocząć wspólną rozmowę', 'Zarządzanie członkami grupy - właściciel grupy może dodawać i usuwać osoby, zmieniać nazwę grupy oraz nadawać rolę administratora z poziomu ikony w nagłówku rozmowy', 'Rola administratora w grupie - administrator może dodawać i usuwać członków oraz zmieniać nazwę grupy, nie będąc właścicielem', ], 'improve': [ 'Wgrywanie logo firmy - przesłane logo jest automatycznie konwertowane do nowoczesnego formatu i od razu widoczne na profilu, bez konieczności odświeżania', 'Licznik uczestników wydarzenia - na stronie głównej i w kalendarzu widoczna jest prawidłowa liczba uczestników, łącznie z zaproszonymi gośćmi', ], }, { 'version': 'v1.61.0', 'date': '7 kwietnia 2026', 'badges': ['improve'], 'links': {}, 'improve': [ 'Szybkość ładowania portalu - strony ładują się zauważalnie szybciej dzięki zmniejszeniu rozmiaru o ponad 250 KB i wyeliminowaniu blokujących zasobów zewnętrznych', 'Czytelność na telefonie - poprawiony kontrast kolorów w stopce strony, zgodnie ze standardami dostępności', ], }, { 'version': 'v1.60.0', 'date': '6 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Płatne wydarzenia': '/kalendarz/', 'Edycja wydarzenia': '/kalendarz/', 'Zapraszanie współpracowników': '/kalendarz/', }, 'new': [ '★ Płatne wydarzenia - organizator może ustawić kwotę za udział w spotkaniu, a przy każdym uczestniku widoczny jest status płatności', 'Edycja wydarzenia - kadra zarządzająca może edytować opis, datę i szczegóły wydarzenia bezpośrednio na jego stronie', 'Zapraszanie współpracowników - przy zapisywaniu się na wydarzenie można od razu wybrać osoby ze swojej firmy i zapisać je jednym kliknięciem', ], 'improve': [ 'Łączna kwota na wydarzeniu - na stronie wydarzenia widoczna jest suma do zapłaty za siebie i zaproszonych gości', ], }, { 'version': 'v1.59.0', 'date': '5 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Nowa strona główna': '/', 'Nowi członkowie na stronie głównej': '/', }, 'new': [ '★ Nowa strona główna - przebudowany układ z dwukolumnowym widokiem wydarzeń, najnowszym tematem z forum i kafelkiem nowych członków Izby', 'Nowi członkowie na stronie głównej - firmy przyjęte na ostatnim posiedzeniu Rady są widoczne na stronie głównej z odnośnikiem do pełnej listy', ], 'improve': [ 'Godziny w kalendarzu - subskrypcja kalendarza (ICS) nie wymyśla godziny zakończenia, gdy organizator jej nie podał', ], }, { 'version': 'v1.58.0', 'date': '4 kwietnia 2026', 'badges': ['new'], 'links': { 'Strona nowych członków': '/nowi-czlonkowie', }, 'new': [ '★ Strona nowych członków - lista firm przyjętych na poszczególnych posiedzeniach Rady Izby, z podziałem na daty i odnośnikami do profili', ], }, { 'version': 'v1.57.0', 'date': '3 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Kreator dodawania firmy': '/admin/company/wizard', }, 'new': [ '★ Kreator dodawania firmy - pięcioetapowy formularz dla biura Izby: wystarczy wpisać NIP, a system pobierze dane z KRS/CEIDG, znajdzie stronę internetową i podpowie kategorię działalności', 'Automatyczne wyszukiwanie strony WWW - kreator szuka strony internetowej firmy na podstawie nazwy i NIP-u, a znalezione dane można zaakceptować jednym kliknięciem', ], 'improve': [ 'Zbieranie informacji o firmie - okno z wynikami wyszukiwania ma zawsze widoczne przyciski akceptacji, nawet przy dłuższych opisach', ], }, { 'version': 'v1.56.0', 'date': '2 kwietnia 2026', 'badges': ['new', 'fix'], 'links': { 'Zarządzanie zespołem firmy': '/firma/edytuj', }, 'new': [ '★ Zarządzanie zespołem firmy - kadra zarządzająca może samodzielnie dodawać nowe osoby do firmy, nadawać im role i zarządzać uprawnieniami, bez pomocy administratora portalu', 'Zaproszenie nowego pracownika - wystarczy podać email i imię — system tworzy konto i wysyła zaproszenie z linkiem do ustawienia hasła', 'Role w zespole - trzy poziomy dostępu: Obserwator (podgląd), Pracownik (edycja wybranych sekcji profilu) i Kadra zarządzająca (pełna kontrola)', 'Uprawnienia pracownika - kadra zarządzająca decyduje, które sekcje profilu firmy może edytować każdy pracownik', 'Status aktywności - przy każdej osobie w zespole widać datę ostatniego logowania, a dla nowych osób możliwość ponownego wysłania zaproszenia', ], 'fix': [ 'Wgrywanie logo - naprawiono błąd, przez który logo nie zapisywało się prawidłowo po przesłaniu pliku', ], }, { 'version': 'v1.55.0', 'date': '1 kwietnia 2026', 'badges': ['new', 'improve'], 'links': { 'Kalendarz wydarzeń': '/kalendarz/', }, 'new': [ '★ Goście na wydarzeniach - organizator może zaprosić gości spoza portalu na wydarzenie, podając imię i email — goście pojawiają się na liście uczestników', 'Skrót „Moja firma" w menu - po kliknięciu w swoje imię w górnym pasku widnieje bezpośredni link do profilu Twojej firmy', ], 'improve': [ 'Okno potwierdzenia - estetyczne okno dialogowe zamiast systemowego komunikatu przeglądarki przy usuwaniu i innych ważnych akcjach', ], }, { 'version': 'v1.54.0', 'date': '31 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Raport składek': '/admin/fees', }, 'new': [ '★ Deklaracje członkowskie jako PDF - każdą deklarację przystąpienia można pobrać jako profesjonalny dokument PDF, gotowy do druku lub archiwizacji', 'Śledzenie statusu deklaracji - wizualny tracker pokazuje, na jakim etapie jest deklaracja (złożona, weryfikacja, zatwierdzona), z datami poszczególnych kroków', 'Historia zmian deklaracji - przy każdej deklaracji widoczna jest pełna historia wydarzeń — kto i kiedy zmienił status', 'Struktura firm w raporcie składek - raport uwzględnia spółki matki i ich marki, z informacją o powiązaniach między firmami', ], 'improve': [ 'Wklejanie zdjęć w wiadomościach - można wkleić obraz ze schowka bezpośrednio do rozmowy, ze zmiennym rozmiarem podglądu', ], }, { 'version': 'v1.53.0', 'date': '28 marca 2026', 'badges': ['new', 'improve'], 'links': { 'NordaGPT': '/chat', }, 'new': [ '★ NordaGPT rozpoznaje firmy - asystent automatycznie dopasowuje pytanie do firm z bazy, nawet gdy nazwa jest wpisana z literówką lub skrótem', '★ Odpowiedzi na żywo - NordaGPT wyświetla odpowiedź słowo po słowie w czasie rzeczywistym, zamiast czekać na całą odpowiedź', 'Propozycje kolejnych pytań - pod każdą odpowiedzią pojawiają się gotowe pytania, które można kliknąć, żeby kontynuować rozmowę', 'Personalne powitanie - NordaGPT zna Twoje imię i firmę, więc odpowiada w kontekście Twojego profilu', ], 'improve': [ 'Dokładniejsze linki do firm - wszystkie linki do firm w odpowiedziach są sprawdzane z bazą danych, dzięki czemu nie prowadzą do nieistniejących stron', 'Szybkość odpowiedzi - proste pytania (np. godziny otwarcia, adres) są obsługiwane natychmiast, złożone pytania strategiczne dostają więcej czasu na przemyślenie', ], }, { 'version': 'v1.52.0', 'date': '27 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Wiadomości': '/wiadomosci', }, 'new': [ '★ Nowy wygląd wiadomości - rozmowy wyglądają teraz jak w popularnych komunikatorach, ze zdjęciami profilowymi i bąbelkami wiadomości', 'Przypinanie wiadomości - ważne wiadomości można przypiąć w rozmowie, a przypięte treści dostępne są z poziomu górnego paska', 'Natychmiastowe wysyłanie - wiadomość pojawia się w rozmowie od razu po kliknięciu Enter, bez opóźnienia', 'Logo firmy - w panelu edycji firmy można dodać logo, które wyświetla się w katalogu i na profilu', 'Mapa witryny - wyszukiwarki (Google, Bing) widzą wszystkie firmy, wydarzenia i tematy na forum, co poprawia widoczność portalu w internecie', ], 'improve': [ 'Potwierdzenie odczytania - pod wiadomością widać dokładną datę i godzinę odczytania, a w grupach — listę osób, które przeczytały', 'Czytelniejsze linki w wiadomościach - linki są wyraźnie widoczne zarówno w jasnych, jak i ciemnych bąbelkach', 'Aktualności na stronie głównej - na telefonie widżet „Co nowego" jest bardziej kompaktowy i nie zasłania treści', ], }, { 'version': 'v1.51.0', 'date': '25 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Kalendarz wydarzeń': '/kalendarz/?view=grid', 'Subskrypcja kalendarza': '/kalendarz/', }, 'new': [ '★ Subskrypcja kalendarza - wydarzenia Izby można dodać do kalendarza w telefonie (iPhone) lub komputerze (Google Calendar), a nowe spotkania będą się pojawiać automatycznie', '★ Zbieranie informacji o firmie - przycisk wyszukuje dane o firmie w internecie i pokazuje wyniki do akceptacji — sam decydujesz, które informacje dodać do profilu', 'Wskaźnik kompletności profilu - na stronie firmy widać, ile procent profilu jest wypełnione i jakich informacji brakuje', 'Powiązania firm w katalogu - na liście firm widać, które firmy są markami tej samej spółki (np. Scantric i You\'re Welcome należą do Forte Media)', ], 'improve': [ 'Kalendarz na telefonie - widok miesięczny działa prawidłowo na małych ekranach, dni z wydarzeniami oznaczone kolorami, kliknięcie w dzień pokazuje listę spotkań', 'Ważne wydarzenia na czerwono - kluczowe wydarzenia (jak prezentacja portalu czy spotkanie z Panią Wojewodą) wyróżnione kolorem czerwonym w kalendarzu', 'Menu użytkownika na telefonie - po kliknięciu w swoje imię opcje konta (Mój Panel, Wiadomości, Wyloguj) wyświetlają się jako wygodny panel wysuwany z dołu ekranu', 'NordaGPT — informacja o limitach - w czacie widoczne jest zużycie dziennego limitu oraz zużycie wszystkich użytkowników portalu, z wyjaśnieniem jak działają limity', 'Informacje ze stron WWW firm - sekcja ze słowami kluczowymi ze strony internetowej firmy pokazuje najważniejsze hasła, z możliwością rozwinięcia pełnej listy', 'Panel edycji firmy - na górze panelu edycji widnieje informacja, co możesz samodzielnie zmienić, a które dane są pobierane automatycznie z rejestrów', ], }, { 'version': 'v1.50.0', 'date': '20 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Wiadomości grupowe': '/wiadomosci/nowa-grupa', 'Wyszukiwanie w wiadomościach': '/wiadomosci', }, 'new': [ '★ Wiadomości grupowe - można utworzyć grupę z dowolną liczbą członków Izby i prowadzić wspólną rozmowę, jak na Teams czy WhatsApp', '★ Wyszukiwanie w wiadomościach - pole szukania na górze skrzynki filtruje wiadomości po temacie, treści i nadawcy, wyniki pojawiają się na bieżąco', 'Status odczytania w grupie - pod każdą wiadomością widać zdjęcia osób, które ją przeczytały', 'Automatyczne odświeżanie czatu - nowe wiadomości w grupie pojawiają się bez konieczności odświeżania strony', 'Usuwanie wiadomości - twórca wiadomości może ją usunąć zarówno w rozmowach grupowych, jak i prywatnych', 'Zarządzanie grupą - właściciel grupy może dodawać i usuwać członków, nadawać rolę moderatora i usunąć całą grupę', ], 'improve': [ 'Sześć wydarzeń na stronie głównej - zamiast czterech, widocznych jest sześć najbliższych wydarzeń w kalendarzu', 'Filtrowanie wydarzeń - przyciski "Norda Biznes" i "Zewnętrzne" na stronie głównej zawsze pokazują do sześciu wydarzeń wybranego typu', 'Zdjęcia profilowe w wiadomościach - zamiast inicjałów wyświetla się zdjęcie użytkownika (jeśli je dodał) w skrzynce, wątkach i na czacie grupowym', 'Klikalne profile w wiadomościach - kliknięcie w zdjęcie lub imię nadawcy prowadzi do profilu tej osoby', ], }, { 'version': 'v1.49.0', 'date': '19 marca 2026', 'badges': ['new', 'improve', 'security'], 'links': { 'Panel składek': '/admin/fees', 'Kalendarz wydarzeń': '/kalendarz/', 'Feed RSS — Wydarzenia': '/feed/events.xml', }, 'new': [ '★ Panel składek członkowskich - przegląd opłaconych i zaległych składek z podziałem na miesiące, możliwość oznaczania wpłat i wysyłania przypomnień', '★ Wydarzenia zewnętrzne w kalendarzu - targi, seminaria i szkolenia od organizacji partnerskich (np. Agencja Rozwoju Pomorza) widoczne w kalendarzu z osobnym oznaczeniem i możliwością wyrażenia zainteresowania', 'Przypomnienia o składkach - administrator może wysłać spersonalizowaną wiadomość z kwotą zaległości do wybranych osób w firmie, z podglądem przed wysłaniem', 'Raport składek dla Rady Izby - raport zbiorczy z wykresami pokazujący stan opłacenia składek i statystyki płatności dostępny dla członków Rady', 'Edytor tekstu w wiadomościach - formatowanie pogrubienia, kursywy, list i linków, wklejanie zdjęć bezpośrednio ze schowka', 'Kanały RSS - portal udostępnia trzy kanały z aktualnościami: wydarzenia Izby, ogłoszenia i wiadomości o elektrowni jądrowej', 'Filtr wydarzeń w kalendarzu - jedno kliknięcie pozwala pokazać lub ukryć wydarzenia zewnętrzne', ], 'improve': [ 'Rok założenia firmy na profilu - właściciel firmy może uzupełnić rok rozpoczęcia działalności w edycji profilu', 'NordaGPT zna więcej o firmach - asystent AI korzysta teraz ze wszystkich pól profilu: rok założenia, liczba pracowników, certyfikaty, realizacje', ], 'security': [ 'Ograniczony dostęp dla gości - osoby bez aktywnego członkostwa widzą tylko podstawowe informacje o firmach, bez dostępu do osób, wiadomości i forum', ], }, { 'version': 'v1.48.0', 'date': '18 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Kalendarz': '/kalendarz/', }, 'new': [ 'Załączniki do wydarzeń - do wydarzeń w kalendarzu można dołączyć pliki, np. agendę czy materiały dla uczestników', 'Eksport do kalendarza osobistego - przycisk "Dodaj do kalendarza" generuje plik ICS z przypomnieniem na dzień przed, lokalizacją i organizatorem', 'Panel aktywności użytkowników - statystyki logowań, najpopularniejsze przeglądarki i urządzenia dostępne dla administratorów', ], 'improve': [ 'Przyciski zapisów na wydarzenia - zielony przycisk "Zapisany" z możliwością rezygnacji po najechaniu kursorem', 'Rozpoznawanie osób w opisach wydarzeń - imiona i nazwiska członków Izby wspomniane w opisie wydarzenia stają się linkami do ich profili', 'Edycja profilu firmy - rok założenia, rozbudowany edytor opisu z formatowaniem tekstu', ], }, { 'version': 'v1.47.0', 'date': '17 marca 2026', 'badges': ['improve'], 'links': { 'Kalendarz': '/kalendarz/', }, 'improve': [ 'Klikalne osoby w kalendarzu - wszyscy zapisani uczestnicy wydarzeń wyświetlani jako kolorowe "plakietki" z linkiem do profilu', 'Ulepszone szczegóły wydarzeń - nowy wygląd strony wydarzenia z logo firmy prelegenta i listą zapisanych osób', ], }, { 'version': 'v1.46.0', 'date': '16 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Sekcja PEJ — Elektrownia Jądrowa': '/pej', 'Local Content': '/pej/local-content', 'Aktualności PEJ': '/pej/aktualnosci', 'Kontakty zewnętrzne': '/kontakty', }, 'new': [ '★ Sekcja PEJ — Elektrownia Jądrowa - nowa przestrzeń poświęcona projektowi budowy pierwszej elektrowni jądrowej w Polsce, z aktualnościami, listą firm gotowych do współpracy i informacjami dla dostawców', '★ Local Content - pełna lista firm z Izby dopasowanych do projektu PEJ z możliwością filtrowania po branży i typie współpracy', 'Multimedia PEJ - filmy o elektrowni jądrowej i infografiki ze schematem bloku oraz harmonogramem budowy', 'Źródła i kontakty PEJ - dane kontaktowe do kluczowych osób w PEJ i Bechtel, linki do platformy zakupowej i rejestracji dostawców', 'Śledzenie przeczytanych artykułów - kliknięty artykuł w aktualnościach PEJ zostaje oznaczony, dzięki czemu widać co już było przeczytane', ], 'improve': [ 'Menu nawigacji - "Kaszubia/PEJ" w jednym miejscu zamiast rozrzuconych linków, mniej pozycji w menu dla lepszej przejrzystości', '66 aktualności nuklearnych - istniejące wiadomości o elektrowni jądrowej zostały automatycznie przypisane do sekcji PEJ', 'Kontakty zewnętrzne - dodano 8 nowych kontaktów związanych z projektem PEJ: osoby w PEJ, Bechtel, PAA i Ministerstwie Przemysłu', 'Roadmapa Kaszubia - zaktualizowana oś czasu z 8 zweryfikowanymi kamieniami milowymi: Baltic Power, elektrownia jądrowa, Kongsberg i offshore', 'Strona Kaszubia - uporządkowana treść, usunięte elementy techniczne, czytelniejszy układ dla członków Izby', ], }, { 'version': 'v1.45.0', 'date': '15 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Projekty na profilu firmy': '/company/rotor', }, 'new': [ 'Monitoring dostępności portalu - panel pokazujący czas działania serwisu w ostatnich 24 godzinach i 30 dniach', 'Statystyki aktywności użytkowników - portal śledzi liczbę logowań i ostatnią aktywność, co pozwala lepiej rozumieć jak członkowie korzystają z platformy', ], 'improve': [ 'Projekty na profilu firmy - na stronie każdej firmy widać do jakich projektów ZOP Kaszubia została dopasowana', 'Ciekawostki na stronie głównej - widżet "Czy wiesz, że..." pokazuje najnowsze fakty z bazy wiedzy o projektach na Kaszubach z możliwością załadowania kolejnych', ], }, { 'version': 'v1.44.0', 'date': '13 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Poradnik Google Reviews': '/edukacja/opinie-google', }, 'new': [ 'Poradnik Google Reviews - w sekcji Edukacja nowy materiał o tym, jak zbierać opinie klientów w Google i dlaczego to ważne dla widoczności firmy', 'Resetowanie hasła przez admina - administrator może wysłać użytkownikowi link do zmiany hasła bez znajomości obecnego hasła', ], 'improve': [ 'Grupowy audyt wizytówek Google - możliwość sprawdzenia wielu wizytówek jednocześnie z podglądem wyników przed zapisem', ], }, { 'version': 'v1.43.0', 'date': '12 marca 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Zdjęcie profilowe': '/konto', 'Profile użytkowników': '/kalendarz', 'Klikalne lokalizacje': '/kalendarz', 'Lista wydarzeń po polsku': '/kalendarz', }, 'new': [ 'Nowości na stronie głównej - pod nagłówkiem katalogu firm wyświetla się informacja o najnowszych zmianach na platformie z linkiem do pełnej listy', 'Polityka prywatności i Regulamin - platforma ma teraz własne dokumenty prawne dostępne w stopce strony', '★ Zdjęcie profilowe - każdy zalogowany użytkownik może dodać swoje zdjęcie, które jest automatycznie przycinane i optymalizowane', '★ Profile użytkowników - po kliknięciu w imię uczestnika wydarzenia lub autora na forum otwiera się strona z jego aktywnością, firmą i możliwością wysłania wiadomości', 'Klikalne lokalizacje - adresy wydarzeń w kalendarzu prowadzą teraz bezpośrednio do Google Maps', 'Klikalne nazwiska - imiona i nazwiska członków Izby wymienione w opisach wydarzeń prowadzą do ich profili', 'Automatyczne linkowanie adresów - adresy stron internetowych w opisach wydarzeń stają się klikalnymi linkami', 'Przyciski kontaktowe na profilu - na stronie osoby widoczne są przyciski do wysłania wiadomości, sprawdzenia firmy i kontaktu', ], 'improve': [ 'Strona wydarzenia - nowy wygląd z logo firmy prelegenta, możliwością dodania do kalendarza i klikalnymi nazwiskami uczestników', 'Lista wydarzeń po polsku - kalendarz wyświetla polskie nazwy miesięcy, dni tygodnia i przycisk zapisania się na wydarzenie', 'Wzbogacone karty osób - profil osoby pokazuje teraz odznakę członka Rady, ostatnią aktywność, liczbę wydarzeń i wpisów na forum', 'Automatyczne łączenie kont - po zalogowaniu portal sam rozpoznaje powiązanie użytkownika z osobą w rejestrze KRS', ], 'fix': [ 'Formatowanie opisów wydarzeń - opisy wpisane jako zwykły tekst wyświetlają się teraz z prawidłowymi akapitami i odstępami', 'Strona błędu - zamiast smutnej buźki wyświetla się logo Norda Biznes', ], }, { 'version': 'v1.42.0', 'date': '11 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Załączniki do wiadomości': '/wiadomosci/nowa', 'Wyszukiwanie odbiorcy': '/wiadomosci/nowa', 'Historia korespondencji': '/wiadomosci', }, 'new': [ '★ Załączniki do wiadomości - do każdej wiadomości prywatnej można dołączyć pliki (dokumenty, zdjęcia, PDF) zarówno przy wysyłaniu nowej wiadomości, jak i przy odpowiedzi', '★ Wyszukiwanie odbiorcy - zamiast rozwijanej listy wystarczy zacząć wpisywać imię, nazwisko lub nazwę firmy, a portal podpowie pasujące osoby', 'Podgląd profilu odbiorcy - po wybraniu odbiorcy wyświetla się karta z jego danymi i firmą, dzięki czemu wiadomo do kogo trafia wiadomość', 'Potwierdzenie odczytania - w skrzynce wysłanych widoczne jest, czy odbiorca przeczytał wiadomość', 'Historia korespondencji - po otwarciu wiadomości widoczny jest cały wątek rozmowy z daną osobą w jednym miejscu', ], 'improve': [ 'Kolorowe statusy wiadomości - wiadomości nowe, wysłane i przeczytane oznaczone są kolorowymi etykietami dla łatwiejszego przeglądania', 'Nowy wygląd skrzynki - skrzynka odbiorcza i wysłane zostały przebudowane dla lepszej czytelności na komputerze i telefonie', ], }, { 'version': 'v1.41.0', 'date': '7 marca 2026', 'badges': ['new', 'improve'], 'links': { 'Oznaczenie „Nowe"': '/pulpit', }, 'new': [ 'Oznaczenie „Nowe" - na pulpicie widoczne jest, które wydarzenia i ogłoszenia pojawiły się od ostatniego logowania', 'Oznaczenia dostępu do wydarzeń - wydarzenia z ograniczonym dostępem (tylko dla Rady lub ukryte) mają widoczne etykiety', ], 'improve': [ 'Więcej wydarzeń na stronie głównej - zamiast dwóch wyświetlają się cztery najbliższe wydarzenia w przejrzystej siatce', ], }, { 'version': 'v1.40.0', 'date': '7 marca 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Sekcja Aktualności': '/ogloszenia', 'Ulepszona sztuczna inteligencja': '/chat', }, 'new': [ 'Sekcja Aktualności - ogłoszenia i informacje dla członków Izby z wyróżnionymi wydarzeniami, zdjęciami i załącznikami', 'Ulepszona sztuczna inteligencja - NordaGPT korzysta teraz z najnowszej generacji modeli Google Gemini 3.1 z lepszym rozumieniem pytań', ], 'improve': [ 'Szybsze audyty SEO - analiza stron internetowych firm korzysta z wydajniejszego parsera HTML', 'Płynniejsze korzystanie z portalu - automatyczne odświeżanie powiadomień i wiadomości nie wpływa już na limit zapytań', ], 'fix': [ 'Kalendarz - naprawiono błąd przy wpisaniu nieprawidłowego roku w adresie strony', 'Audyt SEO - naprawiono zapis danych o wykorzystaniu zapytań do Google PageSpeed', ], }, { 'version': 'v1.39.0', 'date': '23 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Zarządzanie powiadomieniami': '/konto/prywatnosc', }, 'new': [ '★ Powiadomienia e-mail o wiadomościach - po otrzymaniu wiadomości prywatnej przychodzi e-mail z podglądem treści i linkiem do odpowiedzi', 'Zarządzanie powiadomieniami - w ustawieniach prywatności można włączyć lub wyłączyć powiadomienia e-mail o nowych wiadomościach', '★ Instalacja aplikacji na telefonie - na Androidzie wystarczy jedno kliknięcie „Zainstaluj", na iPhonie instrukcja krok po kroku z obrazkami', ], 'improve': [ 'Klikalne linki na forum - adresy stron internetowych wklejone we wpisach na forum automatycznie stają się klikalnymi linkami', 'Godziny otwarcia na żywo - status „otwarte/zamknięte" na profilu firmy obliczany jest na bieżąco na podstawie aktualnej godziny', ], 'fix': [ 'Formatowanie wpisów na forum - poprawiono zbyt duże odstępy między wierszami i prawidłowe wyświetlanie list', 'Podwójne wysyłanie odpowiedzi - naprawiono błąd umożliwiający przypadkowe podwójne wysłanie odpowiedzi na forum', ], }, { 'version': 'v1.38.0', 'date': '22 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Osoby kontaktowe na profilu firmy': '/', }, 'new': [ '★ Osoby kontaktowe na profilu firmy - na profilu każdej firmy widoczne są osoby powiązane z firmą wraz z danymi kontaktowymi i możliwością wysłania wiadomości', 'Wiadomości w menu głównym - nowy skrót do wiadomości prywatnych w górnym menu z licznikiem nieprzeczytanych', 'Powiadomienia o nowych wiadomościach - po otrzymaniu wiadomości prywatnej pojawia się powiadomienie w dzwoneczku', ], 'improve': [ 'Karta kontaktowa na profilu firmy - wyraźne przyciski do wysłania e-maila i wiadomości prywatnej na portalu', 'Oznaczenie członków Norda - osoby powiązane z firmami członkowskimi mają widoczny znaczek „Norda"', 'Podgląd reakcji na forum - po najechaniu na reakcję widoczna lista osób, które zareagowały', ], 'fix': [ 'Podgląd profilu na forum - naprawiono wyświetlanie statystyk po najechaniu na autora wpisu', ], }, { 'version': 'v1.37.0', 'date': '21 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ '★ Wyszukiwanie stron internetowych firm - automatyczne znajdowanie stron WWW dla firm, które ich nie mają w katalogu', 'Audyt SEO portalu - panel do sprawdzania jakości SEO samej platformy Norda Biznes z historią wyników', 'Firmy na stronie głównej - kafelki firm członkowskich widoczne na stronie startowej portalu', 'Nowy wygląd e-maili - ujednolicony, profesjonalny szablon wszystkich wiadomości e-mail z portalu', ], 'improve': [ 'Jakość danych firm - nowy panel z oceną kompletności danych, podpowiedziami i hurtowym uzupełnianiem informacji', 'Poprawne adresy stron firm - linki do stron internetowych działają teraz prawidłowo we wszystkich przypadkach', ], 'fix': [ 'Poprawny adres i telefon Izby - zaktualizowano dane kontaktowe na stronie portalu', 'Linki do Facebooka - naprawiono wyświetlanie odnośnika do strony Norda Biznes na Facebooku', ], }, { 'version': 'v1.36.0', 'date': '20 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ '★ Wykresy analityki postów - 6 wykresów pokazujących zaangażowanie, aktywność publikacji, typy postów i najlepsze dni/godziny', 'Ranking Top 5 postów - najlepiej performujące posty z Facebooka z linkami do oryginału', 'Paginacja listy postów - wybór wyświetlania 10, 20, 50, 100 lub wszystkich postów na stronie', 'Zwijane sekcje - każdą sekcję można zwinąć lub rozwinąć, a ustawienie jest zapamiętywane między wizytami', 'Przycisk "Zaktualizuj dane" - ponowne pobranie wszystkich danych firmy z rejestrów, audytów i mediów społecznościowych', ], 'improve': [ 'Audyt wizytówki Google - czytelniejszy widok wyników i weryfikacja czy znaleziona wizytówka należy do szukanej firmy', 'Wykrywanie profili społecznościowych - naprawiono zapisywanie znalezionych profili Facebook, Instagram, YouTube, LinkedIn i TikTok', 'Pobieranie danych z CEIDG - poprawiono wyszukiwanie jednoosobowych firm w rejestrze CEIDG z pełnym mapowaniem pól', 'Automatyczne wykresy - wykresy wyświetlają się natychmiast po wejściu na stronę bez konieczności klikania', 'Nowa kolejność sekcji - wykresy na górze, posty z aplikacji w środku, posty z Facebooka na dole', ], 'fix': [ 'Logo firm w formacie SVG - naprawiono wyświetlanie logo zapisanych w formacie wektorowym', 'Pobieranie logo - naprawiono błąd uniemożliwiający pobranie logo z niektórych stron internetowych', 'Duplikaty postów z Facebooka - naprawiono problem z pobieraniem tych samych postów wielokrotnie', ], }, { 'version': 'v1.35.0', 'date': '19 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ '★ Moduł Social Media - nowy moduł do zarządzania obecnością firmy w mediach społecznościowych', 'Tworzenie postów z pomocą AI - generowanie treści i hashtagów przez sztuczną inteligencję', 'Wybór tonu komunikacji - 5 stylów pisania od autentycznego przez ekspercki po inspirujący', 'Wybór silnika AI - możliwość wyboru modelu do generowania treści', 'Publikowanie na Facebooku - tworzenie i publikowanie postów bezpośrednio z platformy', 'Widoczność postów - przełączanie między trybem testowym a publicznym dla każdego posta', 'Posty z Facebooka ze statystykami - wyświetlanie polubień, komentarzy, udostępnień i reakcji', 'Diagram cyklu życia posta - wizualizacja procesu od szkicu po publikację', 'Synchronizacja danych z Facebooka - automatyczne pobieranie kategorii, telefonu i adresu ze strony firmowej', 'Integracje dla kierowników - strona integracji dostępna dla osób z rolą Zarządzający', ], 'improve': [ 'Linki social media w postach AI - generowane treści uwzględniają profile firmy w mediach społecznościowych', 'Monitorowanie kosztów AI - informacja o zużyciu zasobów przy generowaniu treści', 'Liczba uczestników przy wydarzeniach - widoczna na pulpicie obok nadchodzących wydarzeń', 'Klikalna karta "Twoja firma" - na pulpicie prowadzi do profilu firmy', 'Ochrona adresów Facebook - identyfikator numeryczny nie nadpisuje czytelnego adresu firmy', ], 'fix': [ 'Połączenie z Facebookiem - naprawiono przechowywanie danych autoryzacji dla niezawodnego publikowania', 'Ponowna próba publikacji - nieudane posty można opublikować ponownie', 'Diagnostyka połączenia z Facebookiem - dodano informacje pomagające rozwiązać problemy z połączeniem', 'Opisy wydarzeń w kalendarzu - poprawiono wyświetlanie formatowanego tekstu w widoku listy', ], }, { 'version': 'v1.34.0', 'date': '18 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Edytor z podglądem na żywo - edycja opisów firmy z formatowaniem tekstu i natychmiastowym podglądem wyniku', '★ Automatyczne pobieranie logo - system pobiera logo ze strony internetowej firmy i proponuje kandydatów z oceną jakości', 'Galeria kandydatów na logo - porównanie wielu propozycji logo przed wyborem najlepszego', 'Notatki administracyjne o firmach - wewnętrzne notatki widoczne tylko dla administratorów', 'Kontrola ról nowych użytkowników - nowi użytkownicy otrzymują ograniczoną rolę do czasu zatwierdzenia przez admina', 'Zmiana roli użytkownika z listy - administrator może zmieniać rolę bezpośrednio w panelu użytkowników', ], 'improve': [ 'Nowy wygląd strony edycji firmy - czytelniejszy układ z kolorową organizacją sekcji', 'Porównanie logo przed zmianą - podgląd dotychczasowego i nowego logo obok siebie', 'Uprawnienia kierowników - osoby zarządzające automatycznie otrzymują pełne uprawnienia przy tworzeniu konta', 'Powiadomienie o nowej rejestracji - naprawiono wysyłanie emaila z alertem o oczekujących zatwierdzeniach', ], 'fix': [ 'Pobieranie danych z CEIDG - naprawiono pobieranie i mapowanie pól z rejestru jednoosobowych firm', 'Audyt SEO - poprawna obsługa wolnych połączeń i stron, które nie istnieją', 'Wyświetlanie formatowanych opisów firm - opisy z edytora wyświetlają się poprawnie na profilu firmy', 'Audyt social media - usunięto źródło generujące zbyt wiele fałszywych wyników', ], }, { 'version': 'v1.33.0', 'date': '17 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Wiele stron internetowych na profilu firmy - możliwość dodania do 5 stron z oznaczeniem typu (główna, e-commerce, blog, portfolio)', 'Kolorowe przyciski typów stron - każdy typ strony ma inny kolor z podpowiedzią po najechaniu', 'Osobne wyniki audytu dla każdej strony - karta wyników SEO wyświetlana oddzielnie dla każdej strony firmy', 'Kontrola widoczności sekcji profilu - właściciel firmy może ukrywać lub pokazywać poszczególne sekcje (rejestry, social media, SEO)', 'Szczegółowa kontrola sekcji rejestrów - oddzielne przełączniki dla KRS, CEIDG i Białej Listy VAT', 'Rozpoznawanie typu profilu LinkedIn - audyt rozróżnia profil firmowy od osobistego', ], 'improve': [ 'Dokładniejsze wykrywanie LinkedIn - lepsze rozpoznawanie profili firmowych w wynikach wyszukiwania', 'Pasek postępu audytu social media - dodatkowy krok wyszukiwania widoczny w pasku postępu', ], 'fix': [ 'Usuwanie stron internetowych firmy - naprawiono usuwanie wszystkich stron przy zapisywaniu zmian', 'Rozdzielanie połączonych adresów - system prawidłowo rozdziela adresy zapisane w jednym polu', 'Wykrywanie LinkedIn - poprawiono dokładność sprawdzania profili LinkedIn', ], }, { 'version': 'v1.32.0', 'date': '14 lutego 2026', 'badges': ['improve', 'fix'], 'improve': [ 'Powiadomienia na telefonie - dropdown wyświetla się jako panel od dołu ekranu zamiast być obcinanym', 'Dodawanie zdjęć na forum z telefonu - przycisk "Dodaj zdjęcie z galerii" zamiast "Ctrl+V" na urządzeniach mobilnych', ], 'fix': [ 'Usuwanie odpowiedzi na forum - naprawiono błąd uniemożliwiający usunięcie odpowiedzi, które były już przeczytane przez innych', 'Wysyłanie załączników z telefonu - naprawiono problem z nieprzesyłaniem zdjęć wybranych z galerii na urządzeniach mobilnych', 'Pulpit użytkownika - naprawiono błąd 500 przy wyświetlaniu dashboardu', ], }, { 'version': 'v1.31.0', 'date': '13 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Widget postępu wdrożenia na pulpicie - widoczny stan konfiguracji firmy po zalogowaniu', 'Zdjęcia w wydarzeniach - wydarzenia mogą zawierać zdjęcia w opisie', 'Nowa firma: UNIMOT Energia i Gaz - logotyp nowej firmy członkowskiej', 'Weryfikacja certyfikatu SSL w audycie SEO - sprawdzanie ważności certyfikatu strony', ], 'improve': [ 'Czytelniejsze opisy wydarzeń - poprawione formatowanie tekstu i wyświetlanie obrazów w kalendarzu', 'Walidacja NIP przy rejestracji - sprawdzanie sumy kontrolnej numeru NIP', ], 'fix': [ 'Audyt SEO - naprawiono ładowanie kluczy API przy uruchamianiu audytu', ], }, { 'version': 'v1.30.0', 'date': '12 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ '3 najbliższe wydarzenia na stronie głównej - szybki podgląd kalendarza bez wchodzenia w szczegóły', 'Nowe firmy: Alter Energy, Fiume Studio - logotypy nowych firm członkowskich', 'Marki zależne dziedziczą NIP - marki podrzędne automatycznie wyświetlają NIP firmy macierzystej', ], 'improve': [ 'Lepsza detekcja profili Facebook w audycie - wykluczenie pixeli śledzących i parametrów Meta z wyników', ], 'fix': [ 'Synchronizacja użytkownik-firma - naprawiono powiązania w tabeli user_companies i nazwy pól kalendarza', ], }, { 'version': 'v1.29.0', 'date': '9 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Analiza planu rozwoju Izby przez AI - sztuczna inteligencja ocenia postęp realizacji strategii i wskazuje braki', 'Panel wiedzy ZOPK na stronie głównej - wybrane wpisy bazy wiedzy widoczne bez wchodzenia w sekcję ZOPK', 'Przyciski zarządzania na stronach ZOPK - administratorzy mogą edytować treści bezpośrednio ze strony publicznej', 'Baza wiedzy ZOPK w czasie rzeczywistym - postęp wyszukiwania i ekstrakcji widoczny na żywo w przeglądarce', '4-etapowy panel zarządzania bazą wiedzy ZOPK - workflow od wyszukiwania przez scraping po ekstrakcję', 'Rozszerzenie Google API do ~100% - GBP Performance i Google Search Console w pełni zintegrowane', 'Kontakt z przełożonym dla pracowników - pracownik widzi dane kontaktowe osoby zarządzającej firmą', 'Pełna matryca uprawnień w panelu dostępu - widoczne wszystkie uprawnienia dla każdej roli', 'Bezpośrednie ustawianie hasła przez admina - bez konieczności wysyłania linku resetującego', 'Kolumna ostatniego logowania w liście użytkowników', ], 'improve': [ 'Automatyczne odświeżanie tokenów OAuth - strona integracji nie wymaga ponownego logowania po wygaśnięciu tokenu', 'Stylizowane okno potwierdzenia rozłączenia OAuth - zamiast standardowego okna przeglądarki', 'Wydzielone przyciski audytu na profilu firmy - przeniesione do osobnego wiersza dla lepszej czytelności', 'Link "Mój pulpit" na profilu firmy - szybki powrót do panelu użytkownika', 'Dostęp do audytów ograniczony do wyznaczonego użytkownika', 'Menu uproszczone dla zwykłych użytkowników - ukrycie opcji widocznych tylko dla właściciela platformy', ], 'fix': [ 'Polskie znaki w audytach - naprawiono wyświetlanie polskich znaków diakrytycznych w szablonach audytu', 'Baza wiedzy ZOPK - naprawiono model embeddingów, scraper treści i polskie komunikaty błędów', 'Pulpit użytkownika - naprawiono błąd wylogowywania przy wyświetlaniu dashboardu z powiązanymi firmami', ], }, { 'version': 'v1.28.0', 'date': '8 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ '★ Audyt SEO: dane z Google Search Console - kliknięcia, wyświetlenia i średnia pozycja z Google', 'Audyt SEO: dane CrUX - rzeczywiste pomiary szybkości od użytkowników Chrome', 'Audyt SEO: nagłówki bezpieczeństwa - sprawdzanie konfiguracji HTTPS i nagłówków ochronnych', 'Audyt SEO: formaty obrazów - analiza wykorzystania nowoczesnych formatów WebP/AVIF', 'Audyt SEO: pasek postępu i podsumowanie wyników - czytelna wizualizacja stanu strony', 'Audyt SEO: instrukcje konfiguracji Search Console - krok po kroku dla firm bez połączonego konta', 'Audyt GBP: migracja na Places API (New) - nowsze i dokładniejsze dane o firmach w Google Maps', 'Audyt GBP: link do recenzji, wskazówki dojazdu, status otwarcia', 'Audyt AI: analiza sentymentu recenzji i benchmarki branżowe', 'Integracje OAuth - framework do łączenia kont Google i Meta z platformą', ], 'improve': [ 'Migracja FID na INP - nowy standard Google do pomiaru interaktywności strony', 'Rozszerzone wyświetlanie danych GBP i Social Media w panelach audytu', ], 'fix': [ 'Audyt GBP - naprawiono fałszywe wykrywanie zdjęć i logo', 'Audyt SEO - naprawiono zamianę klucza fid_ms na inp_ms w trasie', 'Search Console - obsługa formatu domain property (sc-domain:)', ], }, { 'version': 'v1.27.0', 'date': '6 lutego 2026', 'badges': ['security', 'new', 'improve', 'fix'], 'links': { 'Edycja profilu firmy przez właściciela': '/pulpit', 'Pobieranie danych z rejestrów urzędowych': '/', 'NordaGPT zna Izbę NORDA': '/chat', 'Sekcja "Co nowego w Izbie?" na pulpicie': '/pulpit', }, 'security': [ 'Przegląd bezpieczeństwa platformy - naprawiono 8 wykrytych luk (1 krytyczna, 7 średnich)', 'Ochrona wyszukiwarki i bazy wiedzy ZOPK - zabezpieczenie przed atakami przez złośliwe zapytania', 'Bezpieczne zapisywanie treści - oczyszczanie HTML w ogłoszeniach, wydarzeniach i protokołach Rady', 'Ochrona kluczy dostępowych - klucze API nie są już widoczne w logach systemowych', 'Zabezpieczenie formularzy - dodanie ochrony przed nieautoryzowanym wysyłaniem w chacie i 3 formularzach', ], 'new': [ # Forum 'Powiadomienia email z forum - otrzymujesz email gdy ktoś odpowie w temacie, w którym uczestniczysz', 'Automatyczna subskrypcja tematów - po dodaniu odpowiedzi automatycznie śledzisz dalszą dyskusję', 'Rezygnacja z powiadomień - link w każdym emailu pozwala wyłączyć powiadomienia dla danego tematu', # NordaGPT 'NordaGPT zna Izbę NORDA - chatbot odpowiada na pytania o misję, zarząd (16 osób), Akademię NORDA i Chwilę dla Biznesu', 'Strategia 2026-2031 w NordaGPT - chatbot zna 3 kierunki rozwoju Izby, cel 30-lecia i kontekst regionalny Kaszub', 'Projekty członkowskie w NordaGPT - chatbot zna projekty Energo Velo i Żarnowiecki Ring', # Dashboard 'Sekcja "Co nowego w Izbie?" na pulpicie - po zalogowaniu widzisz: wydarzenia, ogłoszenia, tematy forum, oferty B2B i nowe firmy', 'Aktualne dane na pulpicie - liczba nieprzeczytanych powiadomień i nadchodzących wydarzeń zamiast pustych statystyk', # Profil firmy - edycja '★ Edycja profilu firmy przez właściciela - właściciel może sam edytować opisy, usługi, kontakty i social media bez pomocy administratora', 'Podział uprawnień w edycji - dane formalne (NIP, KRS, nazwa) zmienia tylko administrator; dane marketingowe mogą edytować uprawnieni pracownicy', 'Więcej informacji na profilu firmy - wyświetlanie usług, technologii, zasięgu działania, języków, historii i wartości firmy', # Rejestry urzędowe '★ Pobieranie danych z rejestrów urzędowych - administrator jednym kliknięciem pobiera dane firmy z KRS, Białej Listy VAT lub CEIDG', 'Automatyczny dobór rejestru - system sam wybiera właściwy rejestr: KRS dla spółek, CEIDG dla jednoosobowych działalności', 'Import zarządu i branż z KRS - pobierane są osoby w zarządzie i kody PKD (branże działalności)', # Social audit 'Ostrzeżenie o adresie Facebook - audyt wykrywa firmy używające numerycznego ID zamiast własnej nazwy na Facebooku', 'Zalecenia dla Facebooka - podpowiedź aby przekształcić profil osobisty w stronę firmową z czytelnym adresem', 'Zalecenia w panelu admina - administrator widzi kolorowe zalecenia dla każdej firmy (np. brak Facebooka, brak Instagrama)', # Loga firm 'Nowe firmy: Termo i Studio N°33 - dodano logotypy nowych firm członkowskich', ], 'improve': [ 'Nowy wygląd formularza edycji firmy - czytelniejszy układ z zakładkami, ikonami i licznikiem znaków', 'Czytelniejsze podpowiedzi - po najechaniu na awatar na forum i w ogłoszeniach B2B widać czytelną etykietkę z imieniem', 'Lepsza detekcja adresów Facebook - poprawne rozpoznawanie profili z numerycznym ID i nietypowych adresów', 'Zalecenia social media nawet gdy wszystko jest OK - wyświetlanie podpowiedzi np. o zmianie adresu Facebook, nawet gdy firma ma wszystkie platformy', 'Kolumna zaleceń w panelu Social Audit - kolorowe etykiety (czerwone, pomarańczowe, szare, zielone) dla szybkiej oceny', 'Pulpit z dwukolumnowym układem - widgety z aktywnością Izby czytelnie rozmieszczone na ekranie', ], 'fix': [ 'Zatwierdzanie propozycji AI - naprawiono błąd przy klikaniu "Akceptuj i dodaj do profilu"', 'Linki do Facebooka - naprawiono błędne linki dla firm z numerycznym ID na Facebooku', 'Audyt SEO ponownie dostępny - przywrócono działanie usługi audytu SEO po reorganizacji kodu', 'Audyt Google Business Profile ponownie dostępny - przywrócono działanie usługi audytu GBP', 'Wyświetlanie profilu firmy - naprawiono błąd uniemożliwiający otwarcie niektórych profili firm', 'Podpowiedzi na awatarach - poprawiona czytelność etykietek z imionami na forum i w ogłoszeniach', ], }, { 'version': 'v1.26.0', 'date': '5 lutego 2026', 'badges': ['security', 'improve'], 'security': [ 'System uprawnień: 154 trasy zabezpieczone - każda strona administracyjna wymaga teraz odpowiedniego poziomu dostępu', '6-poziomowa hierarchia ról - od zwykłego użytkownika przez członka, pracownika, kierownika po administratora', 'Menu dostosowane do roli - kierownik biura widzi tylko te opcje, do których ma uprawnienia', 'Wybór roli przy tworzeniu użytkownika - zamiast prostego "tak/nie" administrator wybiera konkretny poziom dostępu', ], 'improve': [ '23 testy automatyczne dla systemu ról - weryfikacja poprawności uprawnień na każdym poziomie', 'Trwałe usuwanie firm - zarchiwizowane firmy mogą być trwale usunięte przez administratora (nieodwracalne)', ], }, { 'version': 'v1.25.0', 'date': '4 lutego 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Strefa RADA': '/rada', 'Korzyści dla Członków': '/korzysci', }, 'new': [ '★ Strefa RADA - zamknięta sekcja dla członków Rady Izby z listą posiedzeń i członków', 'Zarządzanie posiedzeniami Rady - program, lista obecności i protokół w jednym miejscu', 'Edytor protokołu - zapisywanie ustaleń, decyzji i zadań z osobą odpowiedzialną i terminem', 'Pobieranie PDF - program posiedzenia i protokół do pobrania jako dokument PDF', 'Lista obecności z kworum - automatyczne liczenie obecnych i sprawdzanie kworum', 'Publikowanie programu i protokołu - osobne publikowanie każdego dokumentu', 'Korzyści dla Członków - oferty partnerskie (WisprFlow AI) dostępne dla członków Izby', 'Strona korzyści - przegląd ofert partnerskich z linkami do wersji demonstracyjnych', 'Ulepszona rejestracja - po weryfikacji email automatyczne zalogowanie i przekierowanie', 'Wydarzenia Rady widoczne tylko dla członków Izby', 'Status wniosku członkowskiego - po złożeniu wniosku widać jego aktualny stan', 'Powiadomienie dla administratora o nowym wniosku członkowskim', 'Szczegóły profilu firmy widoczne tylko dla członków Izby', ], 'improve': [ 'Statusy posiedzeń jako klikalne linki do programu i protokołu', 'Czytelne wyświetlanie przebiegu posiedzenia z decyzjami i zadaniami', 'Środowisko testowe oznaczone wizualnie, aby nie pomylić z produkcją', 'Zablokowane wersje bibliotek dla stabilności platformy', 'Aktualizacja bibliotek systemowych', 'Strefa RADA uproszczona - skupiona na posiedzeniach', 'Korzyści - dane o prowizjach widoczne tylko dla właściciela oferty', 'Trwałe usuwanie firm - administrator może nieodwracalnie usunąć zarchiwizowane firmy', ], 'fix': [ 'Naprawiono zabezpieczenie formularzy publikacji programu i protokołu', 'Naprawiono wyświetlanie posiedzeń bez programu lub punktów obrad', 'Naprawiono przycisk potwierdzenia udziału w wydarzeniach', 'Naprawiono link do składania wniosku członkowskiego', 'Obsługa sytuacji gdy generowanie PDF jest tymczasowo niedostępne', 'Naprawiono błąd przy usuwaniu użytkowników powiązanych z innymi danymi', ], }, { 'version': 'v1.24.0', 'date': '2 lutego 2026', 'badges': ['new', 'improve'], 'new': [ 'Środowisko testowe - osobny serwer do sprawdzania zmian przed wdrożeniem', 'Automatyczne testy - każda zmiana w kodzie jest automatycznie sprawdzana', 'Testy logowania i sesji użytkowników', 'Testy bezpieczeństwa - weryfikacja ochrony przed najczęstszymi atakami', 'Testy w przeglądarce - automatyczne sprawdzanie działania strony', 'Automatyczna weryfikacja po każdym wdrożeniu na produkcję', ], 'improve': [ 'Automatyczna kontrola jakości kodu przed zapisaniem zmian', 'Narzędzia do utrzymania spójności kodu', 'Wskaźnik statusu testów widoczny na stronie projektu', ], }, { 'version': 'v1.23.0', 'date': '1 lutego 2026', 'badges': ['security', 'new', 'improve', 'fix'], 'links': { 'Składanie wniosków o członkostwo': '/rejestracja', }, 'security': [ '6 poziomów dostępu - od gościa przez członka, pracownika, kierownika po administratora', 'NordaGPT dostępny tylko dla członków Izby', 'Wiadomości prywatne tylko dla członków Izby', 'Tablica ogłoszeń B2B tylko dla członków Izby', 'Dane kontaktowe firm widoczne tylko dla członków Izby', ], 'new': [ '★ Składanie wniosków o członkostwo - formularz, weryfikacja danych, zatwierdzanie przez admina', 'Automatyczne wyszukiwanie danych firmy po numerze NIP', 'Porównanie danych podanych przez użytkownika z danymi z rejestrów urzędowych', 'Zatwierdzanie danych z rejestrów - użytkownik może zatwierdzić lub odrzucić pobrane dane', 'Historia procesu - oś czasu wszystkich kroków od złożenia do zatwierdzenia wniosku', 'Powiadomienie dla administratora o decyzji użytkownika ws. danych z rejestrów', 'Sekcja "Dane z rejestrów urzędowych" na profilu firmy (KRS lub CEIDG)', 'Pełne dane z KRS - kapitał zakładowy, sposób reprezentacji, wspólnicy', 'Automatyczny dobór rejestru - KRS dla spółek, CEIDG dla jednoosobowych firm', 'Automatyczne pobieranie danych z KRS przy zatwierdzaniu wniosku', 'Strona promocyjna NordaGPT dla osób niebędących członkami', 'Osobne uprawnienia dla kierownika biura Izby', 'Funkcje portalu wymagające członkostwa w Izbie', 'Panel przypisywania ról użytkownikom', 'Automatyczna aktualizacja opisów firm - AI analizuje strony internetowe członków', ], 'improve': [ 'Czytelniejszy profil firmy - usunięcie powtarzających się informacji', 'Dane kontaktowe zebrane w jednym miejscu na profilu', 'Podział funkcji administracyjnych według poziomu uprawnień', 'Moderacja forum dostępna dla uprawnionych osób', 'Menu dostosowane do uprawnień użytkownika', 'Usunięcie automatycznie generowanych sekcji z profilu firmy', 'Tymczasowe ukrycie sekcji rekomendacji', ], 'fix': [ 'Naprawiono zapisywanie adresu przy tworzeniu nowej firmy', 'Naprawiono linki do profili firm', 'Naprawiono zabezpieczenie formularzy członkostwa', 'Naprawiono błąd przy składaniu wniosku członkowskiego', 'Naprawiono zapisywanie historii procesu członkostwa', 'Naprawiono okno potwierdzenia, które traciło dane po zamknięciu', ], }, { 'version': 'v1.22.0', 'date': '31 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Tablica B2B: Przycisk "Jestem zainteresowany"': '/b2b', 'Forum: Reakcje emoji': '/forum', }, 'new': [ '★ Tablica B2B: Przycisk "Jestem zainteresowany" - wyrażenie zainteresowania ofertą', 'Tablica B2B: Publiczne pytania i odpowiedzi pod ogłoszeniami', 'Tablica B2B: Wysyłanie wiadomości bezpośrednio z ogłoszenia', 'Tablica B2B: Autor widzi kto jest zainteresowany jego ofertą', 'Tablica B2B: Oznaczenie wiadomości powiązanych z ogłoszeniem', 'Forum: Informacja kto przeczytał każdą odpowiedź', 'Tablica B2B: Informacja kto widział ogłoszenie', 'Panel admina: Zarządzanie firmami - lista, edycja, statystyki', 'Panel admina: Zarządzanie osobami - dane z KRS i powiązania z firmami', 'Panel admina: Przegląd stanu platformy - certyfikaty, bezpieczeństwo', 'Rejestr logowań - kto i kiedy się logował do platformy', 'Forum: Reakcje emoji na wpisy i odpowiedzi', 'Forum: Śledzenie tematów z powiadomieniami o nowych odpowiedziach', 'Forum: Edycja własnych wpisów (do 24 godzin)', 'Forum: Zgłaszanie nieodpowiednich treści', 'Forum: Oznaczanie najlepszej odpowiedzi jako rozwiązanie', 'Forum: Statystyki aktywności użytkownika', 'Forum: Formatowanie tekstu (pogrubienie, listy, linki)', 'Forum: Oznaczanie @użytkowników z powiadomieniami', 'Panel admina: Analityka forum - wykresy aktywności i ranking użytkowników', 'Panel admina: Eksport aktywności forum do arkusza', 'Panel admina: Zbiorcze zarządzanie tematami forum', 'Panel admina: Przenoszenie tematów między kategoriami', 'Panel admina: Łączenie powiązanych tematów forum', 'Panel admina: Wyszukiwarka z dostępem do usuniętych treści', 'Panel admina: Historia aktywności użytkowników na forum', 'Panel admina: Przywracanie usuniętych wpisów na forum', 'Menu admina: Szybki dostęp do Forum, Ogłoszeń i Analityki AI', ], 'improve': [ 'Reorganizacja kodu platformy dla łatwiejszego rozwoju', 'Forum: Oznaczenie "(Ty)" przy własnym awatarze', 'Czytelniejszy układ informacji o certyfikatach bezpieczeństwa', 'Porządki w kodzie platformy', ], 'fix': [ 'NordaGPT: Naprawiono pole wpisywania wiadomości, które było ucięte', ], }, { 'version': 'v1.21.0', 'date': '30 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Moje konto': '/konto', }, 'new': [ 'Moje konto - edycja danych osobowych, ustawienia prywatności i bezpieczeństwa', 'Moderacja forum - administrator może usuwać, przypinać i blokować wpisy', 'Moderacja ogłoszeń B2B - administrator może usuwać i dezaktywować ogłoszenia', 'Podgląd hasła - ikonka oka pozwala zobaczyć wpisywane hasło', 'Ładniejsze okna potwierdzenia na forum', 'Ładniejsze okna potwierdzenia w ogłoszeniach B2B', 'Wątek na forum do zgłaszania pomysłów i uwag', ], 'improve': [ 'Poprawna nazwa platformy na stronie rejestracji', 'Przyjazna strona informacyjna podczas aktualizacji platformy', ], 'fix': [ 'Reset hasła nie wymaga już ponownej weryfikacji adresu email', 'Usunięto tymczasowe wideo z sekcji edukacyjnej', ], }, { 'version': 'v1.20.0', 'date': '29 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'Aplikacja mobilna': '/', 'NordaGPT: Nowy silnik AI': '/chat', }, 'new': [ 'NordaGPT: Nowy silnik AI - Google Gemini 3 Flash z lepszym rozumieniem pytań', 'NordaGPT: Dwa tryby - podstawowy (bezpłatny) i zaawansowany (dokładniejszy)', 'NordaGPT: 7x lepsze rozumowanie i dokładniejsze odpowiedzi', 'NordaGPT: Informacja o szacowanym koszcie użytkowania', '★ Aplikacja mobilna - portal można zainstalować na telefonie jak aplikację (iOS/Android)', 'Aktualności: Ogłoszenie może należeć do kilku kategorii jednocześnie', 'Aktualności: Nowe kategorie - Wewnętrzne, Zewnętrzne, Wydarzenie, Okazja biznesowa, Partnerstwo', 'Sekcja edukacyjna: Materiały wideo do obejrzenia na portalu', 'Film powitalny "Wprowadzenie do Norda Biznes Partner"', 'Administrator otrzymuje email o każdej nowej rejestracji', ], 'improve': [ 'Nowa ikona NordaGPT na stronie głównej', 'Porządki w stopce strony', ], 'fix': [ 'Naprawiono błąd przy dodawaniu ogłoszeń B2B', 'Naprawiono błąd przy dodawaniu wydarzeń do kalendarza', 'Naprawiono nawigację w module kontaktów', ], }, { 'version': 'v1.19.0', 'date': '28 stycznia 2026', 'badges': ['new', 'improve', 'security'], 'links': { 'Ukrywanie telefonu i email': '/konto/prywatnosc', 'Blokowanie użytkowników': '/konto/blokady', }, 'new': [ 'Ukrywanie telefonu i email - można wybrać w ustawieniach, co jest widoczne na profilu', 'Blokowanie użytkowników - zablokowana osoba nie może wysyłać wiadomości', 'Wybór preferowanego sposobu kontaktu (email, telefon, portal)', 'Kategorie branżowe - 4 główne grupy z podkategoriami', 'Oznaczenie firm z niekompletnym profilem do uzupełnienia', 'Nowe podkategorie branżowe: Budownictwo, Produkcja, Usługi finansowe', 'Nowa sekcja Edukacja w menu platformy', 'Panel zbierania opinii i pomysłów od użytkowników', 'Rozszerzony monitoring stanu platformy', ], 'improve': [ 'Katalog: Wyraźne oznaczenie wybranej kategorii', 'Kategorie posortowane od największej liczby firm', ], 'security': [ 'Ochrona danych osobowych - chatbot automatycznie ukrywa numery PESEL, karty i IBAN', 'Prywatność rozmów - każdy użytkownik widzi tylko swoje rozmowy z chatbotem', 'Anonimowe statystyki rozmów z chatbotem w panelu admina', ], }, { 'version': 'v1.17.0', 'date': '26 stycznia 2026', 'badges': ['new'], 'links': { 'Sekcja Aktualności': '/ogloszenia', }, 'new': [ 'Sekcja Aktualności - wiadomości i ogłoszenia dla członków Izby', 'Panel zarządzania aktualnościami dla administratora', 'Kategorie aktualności, możliwość przypinania ważnych ogłoszeń', 'Załączniki PDF i linki w aktualnościach', 'Pierwsze ogłoszenia: Baza noclegowa ARP, Konkurs Tytani Przedsiębiorczości', ], }, { 'version': 'v1.16.0', 'date': '14 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Ochrona geograficzna - blokowanie dostępu z krajów wysokiego ryzyka', 'Własna domena email - wiadomości wysyłane z adresu @nordabiznes.pl', 'Raporty - staż członkostwa, obecność w social media, struktura branżowa', 'Data przystąpienia do Izby na profilu firmy z informacją o stażu', 'Pobieranie danych jednoosobowych firm z rejestru CEIDG', 'Panel bezpieczeństwa z oceną poziomu ochrony platformy', ], 'improve': [ 'Uzupełniono rok założenia dla 71 firm (64% katalogowanych)', 'Uzupełniono daty przystąpienia do Izby dla 57 firm (od 1997 roku)', ], 'fix': [ 'Naprawiono wyświetlanie polskich znaków w statystykach', ], }, { 'version': 'v1.15.0', 'date': '13 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'links': { 'NordaGPT zna więcej danych': '/chat', 'Kalendarz: Widok miesięczny': '/kalendarz', }, 'new': [ 'NordaGPT zna więcej danych - rekomendacje, kalendarz, ogłoszenia B2B, forum i dane KRS', 'NordaGPT: Klikalne linki i adresy email w odpowiedziach chatbota', 'NordaGPT: Szybki dostęp do chatbota ze strony głównej', 'Kalendarz: Widok miesięczny z szybkim potwierdzaniem udziału', 'Najbliższe wydarzenie widoczne na stronie głównej z listą uczestników', 'Wzbogacanie profili firm przez AI - automatyczne uzupełnianie informacji z internetu', 'Sprawdzanie danych KRS z raportami postępu', 'Panel analityki - statystyki odwiedzin i aktywności użytkowników', 'Profil firmy: Pełna lista branż (kody PKD) i dane właściciela', 'Zielone oznaczenie przy osobach zweryfikowanych w rejestrze KRS', ], 'improve': [ 'Czytelniejsze formatowanie odpowiedzi NordaGPT', 'Możliwość zwinięcia bannera NordaGPT na stronie głównej', ], 'fix': [ 'Zwiększony limit logowań i audytów SEO', ], }, { 'version': 'v1.14.0', 'date': '12 stycznia 2026', 'badges': ['new', 'improve', 'fix'], 'new': [ 'Audyt wizytówki Google - sprawdzanie kompletności profilu Google dla każdej firmy', 'Poradnik "Jak działa wizytówka Google?" w sekcji audytu', 'Wyniki audytów widoczne bezpośrednio na profilu firmy', ], 'improve': [ 'Jednolita 5-stopniowa skala ocen we wszystkich audytach', 'Wynik audytu social media jako procent zamiast liczby platform', ], 'fix': [ 'Kategorie Google wyświetlane po polsku', ], }, { 'version': 'v1.13.0', 'date': '11 stycznia 2026', 'badges': ['new', 'improve'], 'links': { 'Mapa Powiązań': '/mapa-powiazan', }, 'new': [ '★ Mapa Powiązań - interaktywna wizualizacja powiązań między firmami i osobami', 'Profile osób - dane z rejestrów urzędowych i portalu', 'NordaGPT uczy się z opinii użytkowników i poprawia odpowiedzi', 'Wyszukiwanie osób po częściowym imieniu lub nazwisku', 'Logo firm widoczne w wynikach wyszukiwania', 'Panel użycia AI - statystyki rozmów dla każdego użytkownika', ], 'improve': [ 'Mapa Powiązań: pełnoekranowy widok z podpowiedziami po najechaniu', 'Ładniejsze powiadomienia zamiast systemowych okien', ], }, { 'version': 'v1.11.0', 'date': '10 stycznia 2026', 'badges': ['new', 'improve', 'security'], 'new': [ 'Forum: Wstawianie zdjęć - przeciągnij, wklej ze schowka, do 10 plików', 'Forum: Kategorie wpisów - Propozycja, Błąd, Pytanie', 'Kompletna dokumentacja techniczna platformy', ], 'improve': [ 'Bezpieczne przesyłanie plików ze sprawdzaniem zawartości', ], 'security': [ 'Usunięcie haseł z kodu źródłowego', 'Zmiana hasła bazy danych na serwerze produkcyjnym', ], }, { 'version': 'v1.9.0', 'date': '9 stycznia 2026', 'badges': ['new', 'improve'], 'new': [ 'Audyt wizytówek Google - przegląd profili Google Business wszystkich firm', 'Audyt Social Media - sprawdzanie obecności firm w mediach społecznościowych', 'Tworzenie użytkowników przez AI - wystarczy wkleić tekst lub zrzut ekranu', ], 'improve': [ 'Nowy pasek administracyjny z pogrupowanymi funkcjami', ], }, { 'version': 'v1.8.0', 'date': '8 stycznia 2026', 'badges': ['new'], 'new': [ 'Audyt IT - sprawdzanie infrastruktury informatycznej firm członkowskich', 'Eksport wyników audytu IT do arkusza kalkulacyjnego', ], }, { 'version': 'v1.7.0', 'date': '6 stycznia 2026', 'badges': ['new'], 'new': [ 'Audyt SEO - analiza widoczności stron internetowych firm w wyszukiwarkach', 'Ocena szybkości stron przez Google PageSpeed', ], }, { 'version': 'v1.6.0', 'date': '29 grudnia 2025', 'badges': ['new'], 'new': [ 'Wzmianki medialne - automatyczne wyszukiwanie artykułów o firmach członkowskich', 'Panel moderacji wzmianek dla administratora', 'Wyszukiwanie wzmianek przez wyszukiwarkę Brave', ], }, { 'version': 'v1.5.0', 'date': '15 grudnia 2025', 'badges': ['new', 'improve'], 'new': [ 'Panel Social Media - zarządzanie profilami firm w mediach społecznościowych', 'Sprawdzanie czy profile social media firm są aktywne', ], 'improve': [ 'Sekcja Social Media na profilu firmy', ], }, { 'version': 'v1.4.0', 'date': '1 grudnia 2025', 'badges': ['new'], 'links': { 'Kalendarz wydarzeń': '/kalendarz', }, 'new': [ 'Rekomendacje - firmy mogą polecać się nawzajem', 'Panel składek członkowskich', 'Kalendarz wydarzeń Izby', ], }, { 'version': 'v1.3.0', 'date': '28 listopada 2025', 'badges': ['new', 'improve'], 'links': { 'Chatbot NordaGPT': '/chat', 'Wyszukiwarka firm': '/search', }, 'new': [ '★ Chatbot NordaGPT - asystent AI znający wszystkie firmy członkowskie', 'Wyszukiwarka firm - rozpoznaje synonimy i literówki', ], 'improve': [ 'Szybsza i dokładniejsza wyszukiwarka', ], }, { 'version': 'v1.2.0', 'date': '25 listopada 2025', 'badges': ['new'], 'links': { 'System wiadomości prywatnych': '/wiadomosci', }, 'new': [ 'System wiadomości prywatnych między użytkownikami', 'Powiadomienia o nowych wiadomościach', ], }, { 'version': 'v1.1.0', 'date': '24 listopada 2025', 'badges': ['new', 'improve'], 'new': [ 'Rejestracja i logowanie użytkowników', 'Profile użytkowników powiązane z firmami', ], 'improve': [ 'Strona dostosowana do telefonów i tabletów', ], }, { 'version': 'v1.0.0', 'date': '23 listopada 2025', 'badges': ['new'], 'new': [ 'Oficjalny start platformy Norda Biznes Partner', 'Katalog 111 firm członkowskich', 'Wyszukiwarka firm po nazwie, branży i usługach', 'Profile firm z pełnymi danymi kontaktowymi', ], }, ] @bp.route('/release-notes') def release_notes(): """Historia zmian platformy.""" releases = _get_releases() # Statystyki (używa globalnej stałej COMPANY_COUNT_MARKETING) db = SessionLocal() try: stats = { 'companies': COMPANY_COUNT_MARKETING, 'categories': db.query(Category).filter(Category.parent_id.isnot(None)).count(), } finally: db.close() return render_template('release_notes.html', releases=releases, stats=stats) @bp.route('/polityka-prywatnosci') def polityka_prywatnosci(): """Polityka prywatności platformy.""" return render_template('polityka_prywatnosci.html') @bp.route('/regulamin') def regulamin(): """Regulamin platformy.""" return render_template('regulamin.html') @bp.route('/zainstaluj-aplikacje') def pwa_install(): """Instrukcja instalacji PWA na telefonie.""" return render_template('pwa_install.html') @bp.route('/sw.js') def service_worker(): """Service worker served from root scope for PWA installability.""" return current_app.send_static_file('sw.js'), 200, { 'Content-Type': 'application/javascript', 'Service-Worker-Allowed': '/' } @bp.route('/robots.txt') def robots_txt(): """Robots.txt for search engine crawlers.""" content = """User-agent: * Allow: / Disallow: /admin/ Disallow: /api/ Disallow: /chat Disallow: /login Disallow: /register Disallow: /dashboard Sitemap: https://nordabiznes.pl/sitemap.xml """ return Response(content, mimetype='text/plain') @bp.route('/sitemap.xml') def sitemap_xml(): """Dynamic sitemap XML with all public pages, companies, events, forum topics.""" from database import SessionLocal, Company, NordaEvent, ForumTopic today = date.today().isoformat() base = 'https://nordabiznes.pl' urls = [] # Static pages urls.append(('/', today, 'daily', '1.0')) urls.append(('/szukaj', today, 'daily', '0.8')) urls.append(('/release-notes', today, 'weekly', '0.5')) urls.append(('/zainstaluj-aplikacje', today, 'monthly', '0.4')) urls.append(('/polityka-prywatnosci', today, 'monthly', '0.3')) urls.append(('/regulamin', today, 'monthly', '0.3')) db = SessionLocal() try: # Company pages — highest SEO value companies = db.query(Company).filter( Company.status == 'active' ).all() for c in companies: lastmod = today urls.append((f'/company/{c.slug}', lastmod, 'weekly', '0.9')) # Company list / catalog urls.append(('/', today, 'daily', '1.0')) # Calendar events (public, future + recent past) events = db.query(NordaEvent).filter( NordaEvent.event_date >= date.today() - timedelta(days=90) ).all() for e in events: lastmod = e.event_date.isoformat() if e.event_date else today urls.append((f'/kalendarz/{e.id}', lastmod, 'weekly', '0.6')) # Calendar index urls.append(('/kalendarz/', today, 'weekly', '0.7')) # Forum topics (public) try: topics = db.query(ForumTopic).filter( ForumTopic.is_deleted == False # noqa: E712 ).order_by(ForumTopic.created_at.desc()).limit(100).all() for t in topics: lastmod = t.last_reply_at.date().isoformat() if t.last_reply_at else (t.created_at.date().isoformat() if t.created_at else today) urls.append((f'/forum/temat/{t.id}', lastmod, 'weekly', '0.6')) urls.append(('/forum/', today, 'daily', '0.7')) except Exception: pass # Forum might not have topics # B2B classifieds urls.append(('/b2b', today, 'daily', '0.7')) except Exception: pass finally: db.close() # Build XML xml_parts = ['', ''] seen = set() for loc, lastmod, freq, prio in urls: if loc in seen: continue seen.add(loc) xml_parts.append(f' {base}{loc}{lastmod}{freq}{prio}') xml_parts.append('') return Response('\n'.join(xml_parts), mimetype='application/xml') @bp.route('/api/switch-company/', methods=['POST']) @login_required def switch_company(company_id): """Switch the active company context for multi-company users.""" db = SessionLocal() try: uc = db.query(UserCompany).filter_by( user_id=current_user.id, company_id=company_id ).first() if not uc: flash('Nie masz uprawnień do tej firmy.', 'error') else: session['active_company_id'] = company_id flash(f'Przełączono na firmę: {uc.company.name}', 'info') finally: db.close() return redirect(request.referrer or url_for('dashboard'))