Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Display up to 3 next events with RSVP status instead of just one - Add import script for WhatsApp Norda group data (Feb 2026): events, company updates, Alter Energy, Croatia announcement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1236 lines
58 KiB
Python
1236 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
|
|
|
|
return render_template('company_detail.html',
|
|
company=company,
|
|
company_id=company.id, # For analytics conversion tracking
|
|
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 ''})
|
|
|
|
# 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
|
|
)
|
|
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)
|