nordabiz/blueprints/public/routes.py
Maciej Pienczyn b76d2752c6
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat(dashboard): Add onboarding progress widget
Visual timeline showing company profile completion status:
- 6 steps computed from existing DB data (no new tables)
- Color-coded badges: member/office/auto responsibility
- Collapsible with localStorage persistence
- Green "complete" state when all steps done
- Action links for incomplete member-owned steps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:50:25 +01:00

1249 lines
58 KiB
Python

"""
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
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,
AIChatConversation,
AIChatMessage,
UserSession,
SearchQuery,
MembershipApplication,
Announcement,
ForumTopic,
Classified,
UserNotification,
UserCompany,
)
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
db = SessionLocal()
try:
total_companies = db.query(Company).filter_by(status='active').count()
total_categories = db.query(Category).count()
return render_template(
'landing.html',
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()
# 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) >= 3:
break
# Backward compat — next_event used by other parts
next_event = upcoming_events[0]['event'] if upcoming_events else None
# ZOPK Knowledge facts — admin only widget
zopk_facts = []
if current_user.is_admin:
try:
from database import ZOPKKnowledgeFact, ZOPKNews
zopk_facts = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
ZOPKKnowledgeFact.confidence_score >= 0.5
).order_by(func.random()).limit(3).all()
except Exception:
pass
# 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()
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
)
finally:
db.close()
@bp.route('/company/<int:company_id>')
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'))
# Sprawdź czy użytkownik jest członkiem NORDA (ma firmę lub flagę is_norda_member)
if not current_user.is_norda_member and not current_user.company_id:
flash('Dostęp do katalog firm jest dostępny tylko dla członków Izby NORDA. Złóż deklarację członkowską, aby uzyskać pełny dostęp.', 'info')
return redirect(url_for('membership.apply'))
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 latest website analysis sorted by audit date (consistent with seo_audit_dashboard)
website_analysis = db.query(CompanyWebsiteAnalysis).filter_by(
company_id=company_id
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).first()
# 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 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
# 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,
quality_data=quality_data,
events=events,
website_content=website_content,
ai_insights=ai_insights,
social_media=social_media,
contacts=contacts,
recommendations=recommendations,
people=people,
gbp_audit=gbp_audit,
it_audit=it_audit,
pkd_codes=pkd_codes,
can_enrich=can_enrich,
can_edit_profile=can_edit_profile,
company_managers=company_managers,
is_admin=current_user.is_authenticated and current_user.is_admin
)
finally:
db.close()
@bp.route('/company/<slug>')
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/<int:person_id>')
def person_detail(person_id):
"""Person detail page - shows registry data and portal data if available"""
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()
# Try to find matching user account by name (for portal data)
# This is a simple match - in production might need more sophisticated matching
portal_user = None
name_parts = person.full_name().upper().split()
if len(name_parts) >= 2:
# Try to find user where first/last name matches
potential_users = db.query(User).filter(
User.name.isnot(None)
).all()
for u in potential_users:
if u.name:
user_name_parts = u.name.upper().split()
# Check if at least first and last name match
if len(user_name_parts) >= 2:
if (user_name_parts[-1] in name_parts and # Last name match
any(part in user_name_parts for part in name_parts[:-1])): # First name match
portal_user = u
break
return render_template('person_detail.html',
person=person,
company_roles=company_roles,
portal_user=portal_user
)
finally:
db.close()
@bp.route('/company/<slug>/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)
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()
except Exception as e:
logger.error(f"Search logging error: {e}")
db.rollback()
# 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")
return render_template(
'search_results.html',
companies=companies,
people=people_results,
query=query,
category_id=category_id,
result_count=len(companies)
)
finally:
db.close()
@bp.route('/aktualnosci')
@login_required
def events():
"""Company events and news - latest updates from member companies"""
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 firm członkowskich"""
days = request.args.get('days', 90, type=int)
db = SessionLocal()
try:
cutoff_date = datetime.now() - timedelta(days=days)
new_companies = db.query(Company).filter(
Company.status == 'active',
Company.created_at >= cutoff_date
).order_by(Company.created_at.desc()).all()
return render_template('new_members.html',
companies=new_companies,
days=days,
total=len(new_companies)
)
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
user_event_ids = set()
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}
# 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()
return render_template(
'dashboard.html',
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,
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()
@bp.route('/release-notes')
def release_notes():
"""Historia zmian platformy."""
releases = [
{
'version': 'v1.27.0',
'date': '6 lutego 2026',
'badges': ['security', 'new', 'improve', 'fix'],
'security': [
'<strong>Przegląd bezpieczeństwa platformy</strong> - naprawiono 8 wykrytych luk (1 krytyczna, 7 średnich)',
'<strong>Ochrona wyszukiwarki i bazy wiedzy ZOPK</strong> - zabezpieczenie przed atakami przez złośliwe zapytania',
'<strong>Bezpieczne zapisywanie treści</strong> - oczyszczanie HTML w ogłoszeniach, wydarzeniach i protokołach Rady',
'<strong>Ochrona kluczy dostępowych</strong> - klucze API nie są już widoczne w logach systemowych',
'<strong>Zabezpieczenie formularzy</strong> - dodanie ochrony przed nieautoryzowanym wysyłaniem w chacie i 3 formularzach',
],
'new': [
# Forum
'<strong>Powiadomienia email z forum</strong> - otrzymujesz email gdy ktoś odpowie w temacie, w którym uczestniczysz',
'<strong>Automatyczna subskrypcja tematów</strong> - po dodaniu odpowiedzi automatycznie śledzisz dalszą dyskusję',
'<strong>Rezygnacja z powiadomień</strong> - link w każdym emailu pozwala wyłączyć powiadomienia dla danego tematu',
# NordaGPT
'<strong>NordaGPT zna Izbę NORDA</strong> - chatbot odpowiada na pytania o misję, zarząd (16 osób), Akademię NORDA i Chwilę dla Biznesu',
'<strong>Strategia 2026-2031 w NordaGPT</strong> - chatbot zna 3 kierunki rozwoju Izby, cel 30-lecia i kontekst regionalny Kaszub',
'<strong>Projekty członkowskie w NordaGPT</strong> - chatbot zna projekty Energo Velo i Żarnowiecki Ring',
# Dashboard
'<strong>Sekcja "Co nowego w Izbie?" na pulpicie</strong> - po zalogowaniu widzisz: wydarzenia, ogłoszenia, tematy forum, oferty B2B i nowe firmy',
'<strong>Aktualne dane na pulpicie</strong> - liczba nieprzeczytanych powiadomień i nadchodzących wydarzeń zamiast pustych statystyk',
# Profil firmy - edycja
'<strong>Edycja profilu firmy przez właściciela</strong> - właściciel może sam edytować opisy, usługi, kontakty i social media bez pomocy administratora',
'<strong>Podział uprawnień w edycji</strong> - dane formalne (NIP, KRS, nazwa) zmienia tylko administrator; dane marketingowe mogą edytować uprawnieni pracownicy',
'<strong>Więcej informacji na profilu firmy</strong> - wyświetlanie usług, technologii, zasięgu działania, języków, historii i wartości firmy',
# Rejestry urzędowe
'<strong>Pobieranie danych z rejestrów urzędowych</strong> - administrator jednym kliknięciem pobiera dane firmy z KRS, Białej Listy VAT lub CEIDG',
'<strong>Automatyczny dobór rejestru</strong> - system sam wybiera właściwy rejestr: KRS dla spółek, CEIDG dla jednoosobowych działalności',
'<strong>Import zarządu i branż z KRS</strong> - pobierane są osoby w zarządzie i kody PKD (branże działalności)',
# Social audit
'<strong>Ostrzeżenie o adresie Facebook</strong> - audyt wykrywa firmy używające numerycznego ID zamiast własnej nazwy na Facebooku',
'<strong>Zalecenia dla Facebooka</strong> - podpowiedź aby przekształcić profil osobisty w stronę firmową z czytelnym adresem',
'<strong>Zalecenia w panelu admina</strong> - administrator widzi kolorowe zalecenia dla każdej firmy (np. brak Facebooka, brak Instagrama)',
# Loga firm
'<strong>Nowe firmy: Termo i Studio N°33</strong> - dodano logotypy nowych firm członkowskich',
],
'improve': [
'<strong>Nowy wygląd formularza edycji firmy</strong> - czytelniejszy układ z zakładkami, ikonami i licznikiem znaków',
'<strong>Czytelniejsze podpowiedzi</strong> - po najechaniu na awatar na forum i w ogłoszeniach B2B widać czytelną etykietkę z imieniem',
'<strong>Lepsza detekcja adresów Facebook</strong> - poprawne rozpoznawanie profili z numerycznym ID i nietypowych adresów',
'<strong>Zalecenia social media nawet gdy wszystko jest OK</strong> - wyświetlanie podpowiedzi np. o zmianie adresu Facebook, nawet gdy firma ma wszystkie platformy',
'<strong>Kolumna zaleceń w panelu Social Audit</strong> - kolorowe etykiety (czerwone, pomarańczowe, szare, zielone) dla szybkiej oceny',
'<strong>Pulpit z dwukolumnowym układem</strong> - widgety z aktywnością Izby czytelnie rozmieszczone na ekranie',
],
'fix': [
'<strong>Zatwierdzanie propozycji AI</strong> - naprawiono błąd przy klikaniu "Akceptuj i dodaj do profilu"',
'<strong>Linki do Facebooka</strong> - naprawiono błędne linki dla firm z numerycznym ID na Facebooku',
'<strong>Audyt SEO ponownie dostępny</strong> - przywrócono działanie usługi audytu SEO po reorganizacji kodu',
'<strong>Audyt Google Business Profile ponownie dostępny</strong> - przywrócono działanie usługi audytu GBP',
'<strong>Wyświetlanie profilu firmy</strong> - naprawiono błąd uniemożliwiający otwarcie niektórych profili firm',
'<strong>Podpowiedzi na awatarach</strong> - poprawiona czytelność etykietek z imionami na forum i w ogłoszeniach',
],
},
{
'version': 'v1.26.0',
'date': '5 lutego 2026',
'badges': ['security', 'improve'],
'security': [
'<strong>System uprawnień: 154 trasy zabezpieczone</strong> - każda strona administracyjna wymaga teraz odpowiedniego poziomu dostępu',
'<strong>6-poziomowa hierarchia ról</strong> - od zwykłego użytkownika przez członka, pracownika, kierownika po administratora',
'<strong>Menu dostosowane do roli</strong> - kierownik biura widzi tylko te opcje, do których ma uprawnienia',
'<strong>Wybór roli przy tworzeniu użytkownika</strong> - zamiast prostego "tak/nie" administrator wybiera konkretny poziom dostępu',
],
'improve': [
'<strong>23 testy automatyczne dla systemu ról</strong> - weryfikacja poprawności uprawnień na każdym poziomie',
'<strong>Trwałe usuwanie firm</strong> - zarchiwizowane firmy mogą być trwale usunięte przez administratora (nieodwracalne)',
],
},
{
'version': 'v1.25.0',
'date': '4 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Strefa RADA</strong> - zamknięta sekcja dla członków Rady Izby z listą posiedzeń i członków',
'<strong>Zarządzanie posiedzeniami Rady</strong> - program, lista obecności i protokół w jednym miejscu',
'<strong>Edytor protokołu</strong> - zapisywanie ustaleń, decyzji i zadań z osobą odpowiedzialną i terminem',
'<strong>Pobieranie PDF</strong> - program posiedzenia i protokół do pobrania jako dokument PDF',
'<strong>Lista obecności z kworum</strong> - automatyczne liczenie obecnych i sprawdzanie kworum',
'<strong>Publikowanie programu i protokołu</strong> - osobne publikowanie każdego dokumentu',
'<strong>Korzyści dla Członków</strong> - oferty partnerskie (WisprFlow AI) dostępne dla członków Izby',
'<strong>Strona korzyści</strong> - przegląd ofert partnerskich z linkami do wersji demonstracyjnych',
'<strong>Ulepszona rejestracja</strong> - po weryfikacji email automatyczne zalogowanie i przekierowanie',
'<strong>Wydarzenia Rady</strong> widoczne tylko dla członków Izby',
'<strong>Status wniosku członkowskiego</strong> - po złożeniu wniosku widać jego aktualny stan',
'<strong>Powiadomienie dla administratora</strong> o nowym wniosku członkowskim',
'<strong>Szczegóły profilu firmy</strong> widoczne tylko dla członków Izby',
],
'improve': [
'<strong>Statusy posiedzeń jako klikalne linki</strong> do programu i protokołu',
'<strong>Czytelne wyświetlanie przebiegu posiedzenia</strong> z decyzjami i zadaniami',
'<strong>Środowisko testowe</strong> oznaczone wizualnie, aby nie pomylić z produkcją',
'Zablokowane wersje bibliotek dla stabilności platformy',
'Aktualizacja bibliotek systemowych',
'<strong>Strefa RADA uproszczona</strong> - skupiona na posiedzeniach',
'<strong>Korzyści</strong> - dane o prowizjach widoczne tylko dla właściciela oferty',
'<strong>Trwałe usuwanie firm</strong> - administrator może nieodwracalnie usunąć zarchiwizowane firmy',
],
'fix': [
'<strong>Naprawiono zabezpieczenie formularzy</strong> 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',
'<strong>Naprawiono błąd przy usuwaniu użytkowników</strong> powiązanych z innymi danymi',
],
},
{
'version': 'v1.24.0',
'date': '2 lutego 2026',
'badges': ['new', 'improve'],
'new': [
'<strong>Środowisko testowe</strong> - osobny serwer do sprawdzania zmian przed wdrożeniem',
'<strong>Automatyczne testy</strong> - każda zmiana w kodzie jest automatycznie sprawdzana',
'<strong>Testy logowania i sesji</strong> użytkowników',
'<strong>Testy bezpieczeństwa</strong> - weryfikacja ochrony przed najczęstszymi atakami',
'<strong>Testy w przeglądarce</strong> - automatyczne sprawdzanie działania strony',
'<strong>Automatyczna weryfikacja</strong> 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'],
'security': [
'<strong>6 poziomów dostępu</strong> - od gościa przez członka, pracownika, kierownika po administratora',
'<strong>NordaGPT dostępny tylko dla członków Izby</strong>',
'<strong>Wiadomości prywatne</strong> tylko dla członków Izby',
'<strong>Tablica ogłoszeń B2B</strong> tylko dla członków Izby',
'<strong>Dane kontaktowe firm</strong> widoczne tylko dla członków Izby',
],
'new': [
'<strong>Składanie wniosków o członkostwo</strong> - formularz, weryfikacja danych, zatwierdzanie przez admina',
'<strong>Automatyczne wyszukiwanie danych firmy</strong> po numerze NIP',
'<strong>Porównanie danych</strong> podanych przez użytkownika z danymi z rejestrów urzędowych',
'<strong>Zatwierdzanie danych z rejestrów</strong> - użytkownik może zatwierdzić lub odrzucić pobrane dane',
'<strong>Historia procesu</strong> - oś czasu wszystkich kroków od złożenia do zatwierdzenia wniosku',
'<strong>Powiadomienie dla administratora</strong> o decyzji użytkownika ws. danych z rejestrów',
'<strong>Sekcja "Dane z rejestrów urzędowych"</strong> na profilu firmy (KRS lub CEIDG)',
'<strong>Pełne dane z KRS</strong> - kapitał zakładowy, sposób reprezentacji, wspólnicy',
'<strong>Automatyczny dobór rejestru</strong> - KRS dla spółek, CEIDG dla jednoosobowych firm',
'<strong>Automatyczne pobieranie danych z KRS</strong> 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',
'<strong>Automatyczna aktualizacja opisów firm</strong> - AI analizuje strony internetowe członków',
],
'improve': [
'<strong>Czytelniejszy profil firmy</strong> - 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': [
'<strong>Naprawiono zapisywanie adresu</strong> 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'],
'new': [
'<strong>Tablica B2B: Przycisk "Jestem zainteresowany"</strong> - wyrażenie zainteresowania ofertą',
'<strong>Tablica B2B: Publiczne pytania i odpowiedzi</strong> pod ogłoszeniami',
'<strong>Tablica B2B: Wysyłanie wiadomości</strong> bezpośrednio z ogłoszenia',
'Tablica B2B: Autor widzi kto jest zainteresowany jego ofertą',
'Tablica B2B: Oznaczenie wiadomości powiązanych z ogłoszeniem',
'<strong>Forum: Informacja kto przeczytał</strong> każdą odpowiedź',
'<strong>Tablica B2B: Informacja kto widział</strong> ogłoszenie',
'<strong>Panel admina: Zarządzanie firmami</strong> - lista, edycja, statystyki',
'<strong>Panel admina: Zarządzanie osobami</strong> - dane z KRS i powiązania z firmami',
'<strong>Panel admina: Przegląd stanu platformy</strong> - certyfikaty, bezpieczeństwo',
'<strong>Rejestr logowań</strong> - kto i kiedy się logował do platformy',
'<strong>Forum: Reakcje emoji</strong> na wpisy i odpowiedzi',
'<strong>Forum: Śledzenie tematów</strong> z powiadomieniami o nowych odpowiedziach',
'<strong>Forum: Edycja własnych wpisów</strong> (do 24 godzin)',
'<strong>Forum: Zgłaszanie nieodpowiednich treści</strong>',
'<strong>Forum: Oznaczanie najlepszej odpowiedzi</strong> jako rozwiązanie',
'Forum: Statystyki aktywności użytkownika',
'Forum: Formatowanie tekstu (pogrubienie, listy, linki)',
'Forum: Oznaczanie @użytkowników z powiadomieniami',
'<strong>Panel admina: Analityka forum</strong> - 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': [
'<strong>Reorganizacja kodu platformy</strong> dla łatwiejszego rozwoju',
'Forum: Oznaczenie "(Ty)" przy własnym awatarze',
'Czytelniejszy układ informacji o certyfikatach bezpieczeństwa',
'Porządki w kodzie platformy',
],
'fix': [
'<strong>NordaGPT: Naprawiono pole wpisywania wiadomości</strong>, które było ucięte',
],
},
{
'version': 'v1.21.0',
'date': '30 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Moje konto</strong> - edycja danych osobowych, ustawienia prywatności i bezpieczeństwa',
'<strong>Moderacja forum</strong> - administrator może usuwać, przypinać i blokować wpisy',
'<strong>Moderacja ogłoszeń B2B</strong> - 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': [
'<strong>Reset hasła</strong> 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'],
'new': [
'<strong>NordaGPT: Nowy silnik AI</strong> - Google Gemini 3 Flash z lepszym rozumieniem pytań',
'<strong>NordaGPT: Dwa tryby</strong> - podstawowy (bezpłatny) i zaawansowany (dokładniejszy)',
'NordaGPT: 7x lepsze rozumowanie i dokładniejsze odpowiedzi',
'NordaGPT: Informacja o szacowanym koszcie użytkowania',
'<strong>Aplikacja mobilna</strong> - 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"',
'<strong>Administrator otrzymuje email</strong> 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'],
'new': [
'<strong>Ukrywanie telefonu i email</strong> - można wybrać w ustawieniach, co jest widoczne na profilu',
'<strong>Blokowanie użytkowników</strong> - zablokowana osoba nie może wysyłać wiadomości',
'Wybór preferowanego sposobu kontaktu (email, telefon, portal)',
'<strong>Kategorie branżowe</strong> - 4 główne grupy z podkategoriami',
'Oznaczenie firm z niekompletnym profilem do uzupełnienia',
'Nowe podkategorie branżowe: Budownictwo, Produkcja, Usługi finansowe',
'<strong>Nowa sekcja Edukacja</strong> 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': [
'<strong>Ochrona danych osobowych</strong> - chatbot automatycznie ukrywa numery PESEL, karty i IBAN',
'<strong>Prywatność rozmów</strong> - 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'],
'new': [
'<strong>Sekcja Aktualności</strong> - 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': [
'<strong>Ochrona geograficzna</strong> - blokowanie dostępu z krajów wysokiego ryzyka',
'<strong>Własna domena email</strong> - wiadomości wysyłane z adresu @nordabiznes.pl',
'<strong>Raporty</strong> - 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'],
'new': [
'<strong>NordaGPT zna więcej danych</strong> - rekomendacje, kalendarz, ogłoszenia B2B, forum i dane KRS',
'<strong>NordaGPT: Klikalne linki</strong> i adresy email w odpowiedziach chatbota',
'<strong>NordaGPT: Szybki dostęp</strong> do chatbota ze strony głównej',
'<strong>Kalendarz: Widok miesięczny</strong> z szybkim potwierdzaniem udziału',
'Najbliższe wydarzenie widoczne na stronie głównej z listą uczestników',
'<strong>Wzbogacanie profili firm przez AI</strong> - automatyczne uzupełnianie informacji z internetu',
'<strong>Sprawdzanie danych KRS</strong> z raportami postępu',
'<strong>Panel analityki</strong> - 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': [
'<strong>Audyt wizytówki Google</strong> - 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'],
'new': [
'<strong>Mapa Powiązań</strong> - interaktywna wizualizacja powiązań między firmami i osobami',
'<strong>Profile osób</strong> - dane z rejestrów urzędowych i portalu',
'<strong>NordaGPT uczy się z opinii użytkowników</strong> 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': [
'<strong>Forum: Wstawianie zdjęć</strong> - przeciągnij, wklej ze schowka, do 10 plików',
'<strong>Forum: Kategorie wpisów</strong> - Propozycja, Błąd, Pytanie',
'<strong>Kompletna dokumentacja techniczna</strong> platformy',
],
'improve': [
'Bezpieczne przesyłanie plików ze sprawdzaniem zawartości',
],
'security': [
'<strong>Usunięcie haseł z kodu źródłowego</strong>',
'Zmiana hasła bazy danych na serwerze produkcyjnym',
],
},
{
'version': 'v1.9.0',
'date': '9 stycznia 2026',
'badges': ['new', 'improve'],
'new': [
'<strong>Audyt wizytówek Google</strong> - przegląd profili Google Business wszystkich firm',
'<strong>Audyt Social Media</strong> - sprawdzanie obecności firm w mediach społecznościowych',
'<strong>Tworzenie użytkowników przez AI</strong> - wystarczy wkleić tekst lub zrzut ekranu',
],
'improve': [
'Nowy pasek administracyjny z pogrupowanymi funkcjami',
],
},
{
'version': 'v1.8.0',
'date': '8 stycznia 2026',
'badges': ['new'],
'new': [
'<strong>Audyt IT</strong> - 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': [
'<strong>Audyt SEO</strong> - analiza widoczności stron internetowych firm w wyszukiwarkach',
'<strong>Ocena szybkości stron</strong> przez Google PageSpeed',
],
},
{
'version': 'v1.6.0',
'date': '29 grudnia 2025',
'badges': ['new'],
'new': [
'<strong>Wzmianki medialne</strong> - 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': [
'<strong>Panel Social Media</strong> - 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'],
'new': [
'<strong>Rekomendacje</strong> - firmy mogą polecać się nawzajem',
'<strong>Panel składek członkowskich</strong>',
'<strong>Kalendarz wydarzeń</strong> Izby',
],
},
{
'version': 'v1.3.0',
'date': '28 listopada 2025',
'badges': ['new', 'improve'],
'new': [
'<strong>Chatbot NordaGPT</strong> - asystent AI znający wszystkie firmy członkowskie',
'<strong>Wyszukiwarka firm</strong> - rozpoznaje synonimy i literówki',
],
'improve': [
'Szybsza i dokładniejsza wyszukiwarka',
],
},
{
'version': 'v1.2.0',
'date': '25 listopada 2025',
'badges': ['new'],
'new': [
'<strong>System wiadomości prywatnych</strong> między użytkownikami',
'Powiadomienia o nowych wiadomościach',
],
},
{
'version': 'v1.1.0',
'date': '24 listopada 2025',
'badges': ['new', 'improve'],
'new': [
'<strong>Rejestracja i logowanie</strong> 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': [
'<strong>Oficjalny start platformy Norda Biznes Partner</strong>',
'<strong>Katalog 111 firm członkowskich</strong>',
'Wyszukiwarka firm po nazwie, branży i usługach',
'Profile firm z pełnymi danymi kontaktowymi',
],
},
]
# 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)