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
Link keys must match the <strong>title</strong> text exactly for the template macro to render "Wypróbuj →" buttons next to each item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2623 lines
137 KiB
Python
2623 lines
137 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, current_app, Response, jsonify
|
|
from flask_login import login_required, current_user
|
|
from sqlalchemy import or_, func
|
|
|
|
from . import bp
|
|
from database import (
|
|
SessionLocal,
|
|
Company,
|
|
Category,
|
|
User,
|
|
CompanyRecommendation,
|
|
CompanyEvent,
|
|
CompanyDigitalMaturity,
|
|
CompanyWebsiteAnalysis,
|
|
CompanyQualityTracking,
|
|
CompanyWebsiteContent,
|
|
CompanyAIInsights,
|
|
CompanySocialMedia,
|
|
CompanyContact,
|
|
Person,
|
|
CompanyPerson,
|
|
GBPAudit,
|
|
ITAudit,
|
|
CompanyPKD,
|
|
NordaEvent,
|
|
EventAttendee,
|
|
EventGuest,
|
|
AIChatConversation,
|
|
AIChatMessage,
|
|
UserSession,
|
|
SearchQuery,
|
|
MembershipApplication,
|
|
Announcement,
|
|
ForumTopic,
|
|
Classified,
|
|
UserNotification,
|
|
UserCompany,
|
|
SystemRole,
|
|
)
|
|
from utils.helpers import sanitize_input
|
|
from extensions import limiter
|
|
from search_service import search_companies
|
|
|
|
# Logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Global constant (same as in app.py)
|
|
COMPANY_COUNT_MARKETING = 150
|
|
|
|
|
|
@bp.route('/')
|
|
def index():
|
|
"""Homepage - landing page for guests, company directory for logged in users"""
|
|
if not current_user.is_authenticated:
|
|
# Landing page for guests with public company tiles
|
|
db = SessionLocal()
|
|
try:
|
|
companies = db.query(Company).filter_by(status='active').order_by(Company.name).all()
|
|
total_companies = len(companies)
|
|
total_categories = db.query(Category).count()
|
|
|
|
# Load social media for all companies in one query
|
|
company_social = {}
|
|
if companies:
|
|
social_records = db.query(CompanySocialMedia).filter(
|
|
CompanySocialMedia.company_id.in_([c.id for c in companies])
|
|
).all()
|
|
for sm in social_records:
|
|
company_social.setdefault(sm.company_id, []).append(sm)
|
|
|
|
return render_template(
|
|
'landing.html',
|
|
companies=companies,
|
|
company_social=company_social,
|
|
total_companies=total_companies,
|
|
total_categories=total_categories
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
# Company directory for logged in users
|
|
db = SessionLocal()
|
|
try:
|
|
from datetime import date
|
|
companies = db.query(Company).filter_by(status='active').order_by(Company.name).all()
|
|
|
|
# Build company relationship maps (parent-child)
|
|
company_map = {c.id: c for c in companies}
|
|
company_children = {} # parent_id -> [child names]
|
|
company_parent = {} # child_id -> parent name
|
|
for c in companies:
|
|
if c.parent_company_id:
|
|
parent = company_map.get(c.parent_company_id)
|
|
if parent:
|
|
company_parent[c.id] = parent.name
|
|
company_children.setdefault(c.parent_company_id, []).append(c.name)
|
|
else:
|
|
# Parent may be inactive — look it up
|
|
inactive_parent = db.query(Company).filter_by(id=c.parent_company_id).first()
|
|
if inactive_parent:
|
|
company_parent[c.id] = inactive_parent.name
|
|
|
|
# Get hierarchical categories (main categories with subcategories)
|
|
main_categories = db.query(Category).filter(
|
|
Category.parent_id.is_(None)
|
|
).order_by(Category.display_order, Category.name).all()
|
|
|
|
# All categories for backwards compatibility
|
|
categories = db.query(Category).order_by(Category.sort_order).all()
|
|
|
|
total_companies = len(companies)
|
|
total_categories = len([c for c in categories if db.query(Company).filter_by(category_id=c.id).count() > 0])
|
|
|
|
# Najbliższe wydarzenia (dla bannera "Kto weźmie udział?")
|
|
all_upcoming = db.query(NordaEvent).filter(
|
|
NordaEvent.event_date >= date.today()
|
|
).order_by(NordaEvent.event_date.asc()).all()
|
|
|
|
upcoming_events = []
|
|
for event in all_upcoming:
|
|
if event.can_user_view(current_user):
|
|
registered = db.query(EventAttendee).filter(
|
|
EventAttendee.event_id == event.id,
|
|
EventAttendee.user_id == current_user.id
|
|
).first() is not None
|
|
can_attend = event.can_user_attend(current_user)
|
|
upcoming_events.append({
|
|
'event': event,
|
|
'user_registered': registered,
|
|
'user_can_attend': can_attend,
|
|
})
|
|
if len(upcoming_events) >= 2:
|
|
break
|
|
|
|
# Backward compat — next_event used by other parts
|
|
next_event = upcoming_events[0]['event'] if upcoming_events else None
|
|
|
|
# ZOPK Knowledge facts — widget dla zalogowanych (najnowsze z różnych źródeł)
|
|
zopk_facts = []
|
|
if current_user.is_authenticated:
|
|
try:
|
|
from database import ZOPKKnowledgeFact, ZOPKNews
|
|
# Pobierz najnowszy fakt z każdego z 3 różnych artykułów
|
|
recent_news_ids = db.query(
|
|
ZOPKKnowledgeFact.source_news_id
|
|
).join(ZOPKNews).filter(
|
|
ZOPKKnowledgeFact.confidence_score >= 0.5,
|
|
ZOPKNews.published_at.isnot(None)
|
|
).group_by(
|
|
ZOPKKnowledgeFact.source_news_id, ZOPKNews.published_at
|
|
).order_by(ZOPKNews.published_at.desc()).limit(3).all()
|
|
news_ids = [r[0] for r in recent_news_ids]
|
|
if news_ids:
|
|
for nid in news_ids:
|
|
fact = db.query(ZOPKKnowledgeFact).filter(
|
|
ZOPKKnowledgeFact.source_news_id == nid
|
|
).first()
|
|
if fact:
|
|
zopk_facts.append(fact)
|
|
except Exception:
|
|
db.rollback()
|
|
|
|
# Sprawdź czy użytkownik ma deklarację członkowską w toku
|
|
pending_application = None
|
|
if not current_user.is_norda_member and not current_user.company_id:
|
|
pending_application = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status.in_(['draft', 'submitted', 'under_review', 'pending_user_approval', 'changes_requested'])
|
|
).first()
|
|
|
|
# Latest release for "Co nowego" banner
|
|
all_releases = _get_releases()
|
|
latest_release = all_releases[0] if all_releases else None
|
|
|
|
# Latest forum activity for homepage (newest reply or topic)
|
|
latest_forum_reply = None
|
|
latest_forum_topic_for_reply = None
|
|
try:
|
|
from database import ForumTopic, ForumReply
|
|
# Find the most recent reply
|
|
latest_reply = db.query(ForumReply).filter(
|
|
ForumReply.is_deleted == False
|
|
).order_by(ForumReply.created_at.desc()).first()
|
|
if latest_reply:
|
|
latest_forum_reply = latest_reply
|
|
latest_forum_topic_for_reply = latest_reply.topic
|
|
else:
|
|
# No replies — fall back to newest topic
|
|
newest_topic = db.query(ForumTopic).filter(
|
|
ForumTopic.is_deleted == False
|
|
).order_by(ForumTopic.created_at.desc()).first()
|
|
if newest_topic:
|
|
latest_forum_topic_for_reply = newest_topic
|
|
except Exception:
|
|
pass
|
|
|
|
# New members — find newest board meeting that has admitted companies
|
|
latest_admitted = []
|
|
last_meeting = None
|
|
try:
|
|
from database import BoardMeeting
|
|
from sqlalchemy import exists
|
|
meetings_with_admissions = db.query(BoardMeeting).filter(
|
|
exists().where(Company.admitted_at_meeting_id == BoardMeeting.id)
|
|
).order_by(BoardMeeting.meeting_date.desc()).first()
|
|
if meetings_with_admissions:
|
|
last_meeting = meetings_with_admissions
|
|
latest_admitted = db.query(Company).filter(
|
|
Company.admitted_at_meeting_id == last_meeting.id
|
|
).order_by(Company.name).all()
|
|
except Exception:
|
|
pass
|
|
|
|
return render_template(
|
|
'index.html',
|
|
companies=companies,
|
|
categories=categories,
|
|
main_categories=main_categories,
|
|
total_companies=total_companies,
|
|
total_categories=total_categories,
|
|
next_event=next_event,
|
|
upcoming_events=upcoming_events,
|
|
pending_application=pending_application,
|
|
zopk_facts=zopk_facts,
|
|
latest_release=latest_release,
|
|
company_children=company_children,
|
|
company_parent=company_parent,
|
|
latest_forum_reply=latest_forum_reply,
|
|
latest_forum_topic_for_reply=latest_forum_topic_for_reply,
|
|
latest_admitted=latest_admitted,
|
|
last_meeting=last_meeting
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/api/zopk-facts')
|
|
@login_required
|
|
def api_zopk_facts():
|
|
"""API endpoint for loading more ZOPK facts (for homepage widget)."""
|
|
from database import ZOPKKnowledgeFact, ZOPKNews
|
|
offset = request.args.get('offset', 0, type=int)
|
|
db = SessionLocal()
|
|
try:
|
|
recent_news_ids = db.query(
|
|
ZOPKKnowledgeFact.source_news_id
|
|
).join(ZOPKNews).filter(
|
|
ZOPKKnowledgeFact.confidence_score >= 0.5,
|
|
ZOPKNews.published_at.isnot(None)
|
|
).group_by(
|
|
ZOPKKnowledgeFact.source_news_id, ZOPKNews.published_at
|
|
).order_by(ZOPKNews.published_at.desc()).offset(offset).limit(3).all()
|
|
news_ids = [r[0] for r in recent_news_ids]
|
|
facts = []
|
|
for nid in news_ids:
|
|
fact = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
|
|
ZOPKKnowledgeFact.source_news_id == nid
|
|
).first()
|
|
if fact:
|
|
type_labels = {'investment': 'inwestycja', 'decision': 'decyzja', 'event': 'wydarzenie',
|
|
'milestone': 'kamień milowy', 'statistic': 'dane', 'partnership': 'współpraca',
|
|
'project': 'projekt'}
|
|
facts.append({
|
|
'text': fact.full_text[:200],
|
|
'type': fact.fact_type,
|
|
'type_label': type_labels.get(fact.fact_type, 'fakt'),
|
|
'source_name': fact.source_news.source_name or fact.source_news.source_domain if fact.source_news else '',
|
|
'source_date': fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news and fact.source_news.published_at else '',
|
|
'source_url': fact.source_news.url if fact.source_news else '',
|
|
})
|
|
return jsonify({'facts': facts, 'has_more': len(facts) == 3})
|
|
except Exception:
|
|
db.rollback()
|
|
return jsonify({'facts': [], 'has_more': False})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/api/upcoming-events')
|
|
@login_required
|
|
def api_upcoming_events():
|
|
"""API: Najbliższe wydarzenia z filtrem typu (all/norda/external)"""
|
|
from database import NordaEvent, EventAttendee
|
|
event_filter = request.args.get('filter', 'all')
|
|
limit = min(request.args.get('limit', 6, type=int), 12)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
query = db.query(NordaEvent).filter(NordaEvent.event_date >= date.today())
|
|
if event_filter == 'norda':
|
|
query = query.filter(NordaEvent.is_external == False)
|
|
elif event_filter == 'external':
|
|
query = query.filter(NordaEvent.is_external == True)
|
|
|
|
all_events = query.order_by(NordaEvent.event_date.asc()).all()
|
|
|
|
results = []
|
|
for event in all_events:
|
|
if not event.can_user_view(current_user):
|
|
continue
|
|
registered = db.query(EventAttendee).filter(
|
|
EventAttendee.event_id == event.id,
|
|
EventAttendee.user_id == current_user.id
|
|
).first() is not None
|
|
can_attend = event.can_user_attend(current_user)
|
|
days = ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd']
|
|
|
|
results.append({
|
|
'id': event.id,
|
|
'title': event.title,
|
|
'date': event.event_date.strftime('%d.%m.%Y'),
|
|
'day': days[event.event_date.weekday()],
|
|
'time': event.time_start.strftime('%H:%M') if event.time_start else None,
|
|
'location': (event.location[:30] + '...' if event.location and len(event.location) > 30 else event.location) if event.location else None,
|
|
'attendee_count': event.total_attendee_count,
|
|
'is_external': event.is_external,
|
|
'external_source': event.external_source,
|
|
'access_level': event.access_level,
|
|
'user_registered': registered,
|
|
'user_can_attend': can_attend,
|
|
'url': url_for('calendar.calendar_event', event_id=event.id),
|
|
})
|
|
if len(results) >= limit:
|
|
break
|
|
|
|
return jsonify({'events': results})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/izba/wladze')
|
|
@login_required
|
|
def chamber_authorities():
|
|
"""Władze Izby — Zarząd, Rada, Komisja Rewizyjna, Sąd Koleżeński"""
|
|
from utils.decorators import member_required
|
|
db = SessionLocal()
|
|
try:
|
|
# Group users by chamber role
|
|
all_with_roles = db.query(User).filter(
|
|
User.chamber_role.isnot(None),
|
|
User.is_active == True
|
|
).order_by(User.name).all()
|
|
|
|
groups = {
|
|
'zarzad': {'title': 'Zarząd Izby', 'icon': '👔', 'members': []},
|
|
'rada': {'title': 'Rada Izby', 'icon': '🏛️', 'members': []},
|
|
'komisja': {'title': 'Komisja Rewizyjna', 'icon': '📋', 'members': []},
|
|
'sad': {'title': 'Sąd Koleżeński', 'icon': '⚖️', 'members': []},
|
|
}
|
|
|
|
for u in all_with_roles:
|
|
if u.chamber_role in ('prezes', 'wiceprezes'):
|
|
groups['zarzad']['members'].append(u)
|
|
elif u.chamber_role == 'czlonek_rady':
|
|
groups['rada']['members'].append(u)
|
|
elif u.chamber_role == 'komisja_rewizyjna':
|
|
groups['komisja']['members'].append(u)
|
|
elif u.chamber_role == 'sad_kolezenski':
|
|
groups['sad']['members'].append(u)
|
|
|
|
# Sort zarząd: prezes first, then wiceprezesi
|
|
groups['zarzad']['members'].sort(key=lambda u: (0 if u.chamber_role == 'prezes' else 1, u.name))
|
|
|
|
return render_template('chamber_authorities.html', groups=groups)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<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'))
|
|
|
|
# Guest flag — logged in but not a member (sees limited company profile)
|
|
is_guest = not current_user.is_norda_member and not current_user.company_id and not current_user.has_role(SystemRole.MEMBER)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
flash('Firma nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Load digital maturity data if available
|
|
maturity_data = db.query(CompanyDigitalMaturity).filter_by(company_id=company_id).first()
|
|
# Get all website analyses sorted by audit date (latest first)
|
|
all_analyses = db.query(CompanyWebsiteAnalysis).filter_by(
|
|
company_id=company_id
|
|
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).all()
|
|
|
|
# Build dict: company_website_id → analysis (latest first wins)
|
|
analyses_by_website = {}
|
|
website_analysis = None # backward compat: keep "main" analysis
|
|
for a in all_analyses:
|
|
if a.company_website_id and a.company_website_id not in analyses_by_website:
|
|
analyses_by_website[a.company_website_id] = a
|
|
if website_analysis is None:
|
|
website_analysis = a # first = latest = main analysis
|
|
|
|
# Load quality tracking data
|
|
quality_data = db.query(CompanyQualityTracking).filter_by(company_id=company_id).first()
|
|
|
|
# Load company events (latest 10)
|
|
events = db.query(CompanyEvent).filter_by(company_id=company_id).order_by(
|
|
CompanyEvent.event_date.desc(),
|
|
CompanyEvent.created_at.desc()
|
|
).limit(10).all()
|
|
|
|
# Load website scraping data (most recent)
|
|
website_content = db.query(CompanyWebsiteContent).filter_by(company_id=company_id).order_by(
|
|
CompanyWebsiteContent.scraped_at.desc()
|
|
).first()
|
|
|
|
# Load AI insights
|
|
ai_insights = db.query(CompanyAIInsights).filter_by(company_id=company_id).first()
|
|
|
|
# Load social media profiles
|
|
social_media = db.query(CompanySocialMedia).filter_by(company_id=company_id).all()
|
|
|
|
# Load company contacts (phones, emails with sources)
|
|
contacts = db.query(CompanyContact).filter_by(company_id=company_id).order_by(
|
|
CompanyContact.contact_type,
|
|
CompanyContact.is_primary.desc()
|
|
).all()
|
|
|
|
# Load recommendations (approved only, with recommender details)
|
|
recommendations = db.query(CompanyRecommendation).filter_by(
|
|
company_id=company_id,
|
|
status='approved'
|
|
).join(User, CompanyRecommendation.user_id == User.id).order_by(
|
|
CompanyRecommendation.created_at.desc()
|
|
).all()
|
|
|
|
# Load people connected to company (zarząd, wspólnicy, prokurenci)
|
|
people = db.query(CompanyPerson).filter_by(
|
|
company_id=company_id
|
|
).join(Person, CompanyPerson.person_id == Person.id).order_by(
|
|
CompanyPerson.role_category,
|
|
Person.nazwisko
|
|
).all()
|
|
|
|
# Load portal users linked to this company (for "Osoby kontaktowe" section)
|
|
company_users = db.query(UserCompany).filter_by(
|
|
company_id=company_id
|
|
).join(User, UserCompany.user_id == User.id).filter(
|
|
User.is_active == True
|
|
).order_by(
|
|
UserCompany.role.desc(), # MANAGER first, then EMPLOYEE
|
|
User.name
|
|
).all()
|
|
# Eager-load user attributes before session close
|
|
for uc in company_users:
|
|
_ = uc.user.name, uc.user.email, uc.user.phone, uc.user.is_norda_member
|
|
_ = uc.user.privacy_show_phone, uc.user.privacy_show_email
|
|
_ = uc.user.contact_prefer_email, uc.user.contact_prefer_phone
|
|
_ = uc.user.contact_prefer_portal, uc.user.contact_note
|
|
|
|
# Load GBP audit (most recent)
|
|
gbp_audit = db.query(GBPAudit).filter_by(
|
|
company_id=company_id
|
|
).order_by(GBPAudit.audit_date.desc()).first()
|
|
|
|
# Load IT audit (most recent)
|
|
it_audit = db.query(ITAudit).filter_by(
|
|
company_id=company_id
|
|
).order_by(ITAudit.audit_date.desc()).first()
|
|
|
|
# Load PKD codes (all - primary first)
|
|
pkd_codes = db.query(CompanyPKD).filter_by(
|
|
company_id=company_id
|
|
).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all()
|
|
|
|
# Check if current user can enrich company data (user with company edit rights)
|
|
can_enrich = False
|
|
can_edit_profile = False
|
|
company_managers = []
|
|
if current_user.is_authenticated:
|
|
can_enrich = current_user.can_edit_company(company.id)
|
|
can_edit_profile = current_user.can_manage_company(company.id)
|
|
|
|
# If user is a member but not manager, load managers for contact modal
|
|
if not can_edit_profile:
|
|
is_company_member = any(
|
|
assoc.company_id == company.id
|
|
for assoc in (current_user.company_associations or [])
|
|
) or current_user.company_id == company.id
|
|
|
|
if is_company_member:
|
|
company_managers = db.query(User).join(UserCompany).filter(
|
|
UserCompany.company_id == company.id,
|
|
UserCompany.role == 'MANAGER'
|
|
).all()
|
|
for m in company_managers:
|
|
_ = m.name, m.email # force-load before session close
|
|
|
|
# ZOPK project links — powiązania z projektami ZOPK
|
|
zopk_links = []
|
|
try:
|
|
from database import ZOPKCompanyLink, ZOPKProject
|
|
zopk_links = db.query(ZOPKCompanyLink).join(ZOPKProject).filter(
|
|
ZOPKCompanyLink.company_id == company_id,
|
|
ZOPKCompanyLink.relevance_score >= 40
|
|
).order_by(ZOPKCompanyLink.relevance_score.desc()).all()
|
|
# Eager-load project data
|
|
for link in zopk_links:
|
|
_ = link.project.name, link.project.slug, link.project.project_type
|
|
except Exception:
|
|
pass
|
|
|
|
# Profile completeness for AI enrichment button
|
|
profile_fields = {
|
|
'Opis firmy': bool(ai_insights and ai_insights.business_summary) or bool(company.description_full),
|
|
'Usługi': bool(ai_insights and ai_insights.services_list) or bool(company.services_offered),
|
|
'Grupa docelowa': bool(ai_insights and ai_insights.target_market),
|
|
'Wyróżniki firmy': bool(ai_insights and ai_insights.unique_selling_points),
|
|
'Wartości firmy': bool(ai_insights and ai_insights.company_values),
|
|
'Tagi branżowe': bool(ai_insights and ai_insights.industry_tags),
|
|
}
|
|
profile_filled = sum(1 for v in profile_fields.values() if v)
|
|
profile_total = len(profile_fields)
|
|
profile_missing = [k for k, v in profile_fields.items() if not v]
|
|
|
|
# For child brands — inherit NIP from parent for display/enrichment
|
|
effective_nip = company.nip
|
|
if not effective_nip and company.parent_company_id:
|
|
parent = db.query(Company).filter_by(id=company.parent_company_id).first()
|
|
if parent:
|
|
effective_nip = parent.nip
|
|
|
|
return render_template('company_detail.html',
|
|
company=company,
|
|
company_id=company.id, # For analytics conversion tracking
|
|
effective_nip=effective_nip,
|
|
maturity_data=maturity_data,
|
|
website_analysis=website_analysis,
|
|
analyses_by_website=analyses_by_website,
|
|
quality_data=quality_data,
|
|
events=events,
|
|
website_content=website_content,
|
|
ai_insights=ai_insights,
|
|
social_media=social_media,
|
|
contacts=contacts,
|
|
recommendations=recommendations,
|
|
people=people,
|
|
company_users=company_users,
|
|
gbp_audit=gbp_audit,
|
|
it_audit=it_audit,
|
|
pkd_codes=pkd_codes,
|
|
can_enrich=can_enrich,
|
|
can_edit_profile=can_edit_profile,
|
|
profile_filled=profile_filled,
|
|
profile_total=profile_total,
|
|
profile_missing=profile_missing,
|
|
company_managers=company_managers,
|
|
is_admin=current_user.is_authenticated and current_user.is_admin,
|
|
zopk_links=zopk_links,
|
|
is_guest=is_guest,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<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>')
|
|
@login_required
|
|
def person_detail(person_id):
|
|
"""Person detail page - shows registry data and portal data if available"""
|
|
if not current_user.is_norda_member and not current_user.has_role(SystemRole.MEMBER):
|
|
flash('Profile osób są dostępne tylko dla członków Izby NORDA.', 'info')
|
|
return redirect(url_for('membership.apply'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Get person with their company relationships
|
|
person = db.query(Person).filter_by(id=person_id).first()
|
|
if not person:
|
|
flash('Osoba nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Get company roles with company details (only active companies)
|
|
company_roles = db.query(CompanyPerson).filter_by(
|
|
person_id=person_id
|
|
).join(Company, CompanyPerson.company_id == Company.id).filter(
|
|
Company.status == 'active'
|
|
).order_by(
|
|
CompanyPerson.role_category,
|
|
Company.name
|
|
).all()
|
|
|
|
# Find matching user account - prefer direct person_id link, then name match
|
|
portal_user = db.query(User).filter(
|
|
User.person_id == person_id
|
|
).first()
|
|
|
|
if not portal_user:
|
|
name_parts = person.full_name().upper().split()
|
|
if len(name_parts) >= 2:
|
|
candidates = []
|
|
for u in db.query(User).filter(User.name.isnot(None)).all():
|
|
if u.name:
|
|
user_name_parts = u.name.upper().split()
|
|
if len(user_name_parts) >= 2:
|
|
if (user_name_parts[-1] in name_parts and
|
|
any(part in user_name_parts for part in name_parts[:-1])):
|
|
candidates.append(u)
|
|
if candidates:
|
|
# Prefer user with person_id set, then non-gmail accounts
|
|
candidates.sort(key=lambda u: (
|
|
0 if u.person_id else 1,
|
|
0 if not u.email.endswith('@gmail.com') else 1,
|
|
u.id,
|
|
))
|
|
portal_user = candidates[0]
|
|
|
|
# Extra data if portal user exists
|
|
is_rada_member = False
|
|
last_active_label = None
|
|
attended_events = []
|
|
forum_topics_count = 0
|
|
forum_replies_count = 0
|
|
|
|
if portal_user:
|
|
is_rada_member = getattr(portal_user, 'is_rada_member', False)
|
|
|
|
# Last activity label
|
|
from datetime import datetime, timedelta
|
|
if portal_user.last_login:
|
|
diff = datetime.now() - portal_user.last_login
|
|
if diff < timedelta(hours=1):
|
|
last_active_label = 'Aktywny teraz'
|
|
elif diff < timedelta(days=1):
|
|
hours = int(diff.total_seconds() // 3600)
|
|
last_active_label = f'Aktywny {hours} godz. temu'
|
|
elif diff < timedelta(days=7):
|
|
days = diff.days
|
|
last_active_label = f'Aktywny {days} dni temu'
|
|
elif diff < timedelta(days=60):
|
|
weeks = diff.days // 7
|
|
last_active_label = f'Aktywny {weeks} tyg. temu'
|
|
else:
|
|
last_active_label = f'Ostatnio: {portal_user.last_login.strftime("%d.%m.%Y")}'
|
|
|
|
# Events attended
|
|
from database import EventAttendee, NordaEvent, ForumTopic, ForumReply
|
|
attended_events = db.query(NordaEvent).join(
|
|
EventAttendee, EventAttendee.event_id == NordaEvent.id
|
|
).filter(
|
|
EventAttendee.user_id == portal_user.id,
|
|
EventAttendee.status == 'confirmed',
|
|
).order_by(NordaEvent.event_date.desc()).limit(5).all()
|
|
|
|
# Forum stats
|
|
forum_topics_count = db.query(ForumTopic).filter_by(
|
|
author_id=portal_user.id, is_deleted=False
|
|
).count()
|
|
forum_replies_count = db.query(ForumReply).filter_by(
|
|
author_id=portal_user.id, is_deleted=False
|
|
).count()
|
|
|
|
return render_template('person_detail.html',
|
|
person=person,
|
|
company_roles=company_roles,
|
|
portal_user=portal_user,
|
|
is_rada_member=is_rada_member,
|
|
last_active_label=last_active_label,
|
|
attended_events=attended_events,
|
|
forum_topics_count=forum_topics_count,
|
|
forum_replies_count=forum_replies_count,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/profil/<int:user_id>')
|
|
@login_required
|
|
def user_profile(user_id):
|
|
"""User profile page - redirects to person_detail if person_id exists,
|
|
otherwise shows a simple profile from User data."""
|
|
if not current_user.is_norda_member and not current_user.has_role(SystemRole.MEMBER):
|
|
flash('Profile osób są dostępne tylko dla członków Izby NORDA.', 'info')
|
|
return redirect(url_for('membership.apply'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter_by(id=user_id).first()
|
|
if not user:
|
|
flash('Użytkownik nie znaleziony.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# If user has person_id, redirect to full person profile
|
|
if user.person_id:
|
|
return redirect(url_for('person_detail', person_id=user.person_id))
|
|
|
|
# Build simple profile from User data
|
|
from datetime import datetime, timedelta
|
|
from database import EventAttendee, NordaEvent, ForumTopic, ForumReply, UserCompany, CompanyRole
|
|
|
|
# Last activity label
|
|
last_active_label = None
|
|
if user.last_login:
|
|
diff = datetime.now() - user.last_login
|
|
if diff < timedelta(hours=1):
|
|
last_active_label = 'Aktywny teraz'
|
|
elif diff < timedelta(days=1):
|
|
hours = int(diff.total_seconds() // 3600)
|
|
last_active_label = f'Aktywny {hours} godz. temu'
|
|
elif diff < timedelta(days=7):
|
|
last_active_label = f'Aktywny {diff.days} dni temu'
|
|
elif diff < timedelta(days=60):
|
|
last_active_label = f'Aktywny {diff.days // 7} tyg. temu'
|
|
else:
|
|
last_active_label = f'Ostatnio: {user.last_login.strftime("%d.%m.%Y")}'
|
|
|
|
# Company associations
|
|
user_companies = db.query(UserCompany).filter_by(user_id=user.id).all()
|
|
|
|
# Events attended
|
|
attended_events = db.query(NordaEvent).join(
|
|
EventAttendee, EventAttendee.event_id == NordaEvent.id
|
|
).filter(
|
|
EventAttendee.user_id == user.id,
|
|
EventAttendee.status == 'confirmed',
|
|
).order_by(NordaEvent.event_date.desc()).limit(5).all()
|
|
|
|
# Forum stats
|
|
forum_topics_count = db.query(ForumTopic).filter_by(
|
|
author_id=user.id, is_deleted=False).count()
|
|
forum_replies_count = db.query(ForumReply).filter_by(
|
|
author_id=user.id, is_deleted=False).count()
|
|
|
|
return render_template('user_profile.html',
|
|
profile_user=user,
|
|
last_active_label=last_active_label,
|
|
user_companies=user_companies,
|
|
attended_events=attended_events,
|
|
forum_topics_count=forum_topics_count,
|
|
forum_replies_count=forum_replies_count,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<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)
|
|
search_query_id = None
|
|
if query:
|
|
try:
|
|
analytics_session_id = session.get('analytics_session_id')
|
|
session_db_id = None
|
|
if analytics_session_id:
|
|
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
|
|
if user_session:
|
|
session_db_id = user_session.id
|
|
|
|
search_query = SearchQuery(
|
|
session_id=session_db_id,
|
|
user_id=current_user.id if current_user.is_authenticated else None,
|
|
query=query[:500],
|
|
query_normalized=query.lower().strip()[:500],
|
|
results_count=len(companies),
|
|
has_results=len(companies) > 0,
|
|
search_type='main',
|
|
filters_used={'category_id': category_id} if category_id else None
|
|
)
|
|
db.add(search_query)
|
|
db.commit()
|
|
search_query_id = search_query.id
|
|
except Exception as e:
|
|
logger.error(f"Search logging error: {e}")
|
|
db.rollback()
|
|
search_query_id = None
|
|
|
|
# For debugging/analytics - log search stats
|
|
if query:
|
|
match_types = {}
|
|
for r in results:
|
|
match_types[r.match_type] = match_types.get(r.match_type, 0) + 1
|
|
logger.info(f"Search '{query}': {len(companies)} results, types: {match_types}")
|
|
|
|
# Search people by name (partial match)
|
|
people_results = []
|
|
if query and len(query) >= 2:
|
|
q = f"%{query}%"
|
|
people_results = db.query(Person).filter(
|
|
or_(
|
|
Person.imiona.ilike(q),
|
|
Person.nazwisko.ilike(q),
|
|
func.concat(Person.imiona, ' ', Person.nazwisko).ilike(q)
|
|
)
|
|
).limit(20).all()
|
|
|
|
# For each person, get their company connections count
|
|
for person in people_results:
|
|
person.company_count = len(set(
|
|
r.company_id for r in person.company_roles
|
|
if r.company and r.company.status == 'active'
|
|
))
|
|
|
|
logger.info(f"Search '{query}': {len(people_results)} people found")
|
|
|
|
# Also search portal users (not yet in people_results via person_id)
|
|
user_results = []
|
|
if query and len(query) >= 2:
|
|
q = f"%{query}%"
|
|
matched_users = db.query(User).filter(
|
|
User.is_active == True,
|
|
User.is_verified == True,
|
|
User.name.ilike(q)
|
|
).limit(20).all()
|
|
|
|
# Exclude users who are already in people_results (via person_id)
|
|
people_person_ids = {p.id for p in people_results}
|
|
for u in matched_users:
|
|
if u.person_id and u.person_id in people_person_ids:
|
|
continue # already shown as Person
|
|
user_results.append(u)
|
|
|
|
if user_results:
|
|
logger.info(f"Search '{query}': {len(user_results)} portal users found")
|
|
|
|
return render_template(
|
|
'search_results.html',
|
|
companies=companies,
|
|
people=people_results,
|
|
user_results=user_results,
|
|
query=query,
|
|
category_id=category_id,
|
|
result_count=len(companies),
|
|
search_query_id=search_query_id
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/aktualnosci')
|
|
@login_required
|
|
def events():
|
|
"""DEPRECATED (2026-03-12): Sekcja nieużywana, zastąpiona przez /ogloszenia (Announcements).
|
|
Route zachowany dla kompatybilności wstecznej - nie rozwijać."""
|
|
event_type_filter = request.args.get('type', '')
|
|
company_id = request.args.get('company', type=int)
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = 20
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Build query
|
|
query = db.query(CompanyEvent).join(Company)
|
|
|
|
# Apply filters
|
|
if event_type_filter:
|
|
query = query.filter(CompanyEvent.event_type == event_type_filter)
|
|
if company_id:
|
|
query = query.filter(CompanyEvent.company_id == company_id)
|
|
|
|
# Order by date (newest first)
|
|
query = query.order_by(
|
|
CompanyEvent.event_date.desc(),
|
|
CompanyEvent.created_at.desc()
|
|
)
|
|
|
|
# Pagination
|
|
total_events = query.count()
|
|
events = query.limit(per_page).offset((page - 1) * per_page).all()
|
|
|
|
# Get companies with events for filter dropdown
|
|
companies_with_events = db.query(Company).join(CompanyEvent).distinct().order_by(Company.name).all()
|
|
|
|
# Event type statistics
|
|
event_types = db.query(
|
|
CompanyEvent.event_type,
|
|
func.count(CompanyEvent.id)
|
|
).group_by(CompanyEvent.event_type).all()
|
|
|
|
return render_template(
|
|
'events.html',
|
|
events=events,
|
|
companies_with_events=companies_with_events,
|
|
event_types=event_types,
|
|
event_type_filter=event_type_filter,
|
|
company_id=company_id,
|
|
page=page,
|
|
per_page=per_page,
|
|
total_events=total_events,
|
|
total_pages=(total_events + per_page - 1) // per_page
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/nowi-czlonkowie')
|
|
@login_required
|
|
def new_members():
|
|
"""Lista nowych członków Izby — pogrupowana wg posiedzeń Rady."""
|
|
db = SessionLocal()
|
|
try:
|
|
from database import BoardMeeting
|
|
from sqlalchemy import exists
|
|
|
|
# Get meetings that have admitted companies, newest first
|
|
meetings = db.query(BoardMeeting).filter(
|
|
exists().where(Company.admitted_at_meeting_id == BoardMeeting.id)
|
|
).order_by(BoardMeeting.meeting_date.desc()).limit(12).all()
|
|
|
|
# For each meeting, get admitted companies
|
|
meetings_data = []
|
|
total = 0
|
|
for meeting in meetings:
|
|
companies = db.query(Company).filter(
|
|
Company.admitted_at_meeting_id == meeting.id
|
|
).order_by(Company.name).all()
|
|
meetings_data.append({
|
|
'meeting': meeting,
|
|
'companies': companies
|
|
})
|
|
total += len(companies)
|
|
|
|
return render_template('new_members.html',
|
|
meetings_data=meetings_data,
|
|
total=total
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/mapa-polaczen')
|
|
def connections_map():
|
|
"""Company-person connections visualization page"""
|
|
return render_template('connections_map.html')
|
|
|
|
|
|
@bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""User dashboard"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get user's conversations
|
|
conversations = db.query(AIChatConversation).filter_by(
|
|
user_id=current_user.id
|
|
).order_by(AIChatConversation.updated_at.desc()).limit(10).all()
|
|
|
|
# Stats
|
|
total_conversations = db.query(AIChatConversation).filter_by(user_id=current_user.id).count()
|
|
total_messages = db.query(AIChatMessage).join(AIChatConversation).filter(
|
|
AIChatConversation.user_id == current_user.id
|
|
).count()
|
|
|
|
# New stats
|
|
unread_notifications = db.query(UserNotification).filter(
|
|
UserNotification.user_id == current_user.id,
|
|
UserNotification.is_read == False
|
|
).count()
|
|
|
|
upcoming_events_count = db.query(NordaEvent).filter(
|
|
NordaEvent.event_date >= date.today()
|
|
).count()
|
|
|
|
user_forum_topics_count = db.query(ForumTopic).filter(
|
|
ForumTopic.author_id == current_user.id,
|
|
ForumTopic.is_deleted == False
|
|
).count()
|
|
|
|
# Check for membership application status
|
|
has_pending_application = False
|
|
has_draft_application = False
|
|
pending_application = None
|
|
|
|
try:
|
|
from database import MembershipApplication
|
|
pending_application = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status.in_(['submitted', 'under_review', 'changes_requested'])
|
|
).first()
|
|
has_pending_application = pending_application is not None
|
|
|
|
if not has_pending_application:
|
|
draft = db.query(MembershipApplication).filter(
|
|
MembershipApplication.user_id == current_user.id,
|
|
MembershipApplication.status == 'draft'
|
|
).first()
|
|
has_draft_application = draft is not None
|
|
except Exception:
|
|
pass # MembershipApplication table may not exist yet
|
|
|
|
# Load user's company associations (multi-company support)
|
|
from sqlalchemy.orm import joinedload
|
|
user_companies = db.query(UserCompany).options(
|
|
joinedload(UserCompany.company)
|
|
).filter_by(
|
|
user_id=current_user.id
|
|
).order_by(UserCompany.is_primary.desc(), UserCompany.created_at.asc()).all()
|
|
# Force-load company names before session closes
|
|
for uc in user_companies:
|
|
_ = uc.company.name if uc.company else None
|
|
|
|
# Managers map for companies where user is EMPLOYEE (for edit permission modal)
|
|
company_managers_map = {}
|
|
employee_company_ids = [uc.company_id for uc in user_companies if uc.role == 'EMPLOYEE']
|
|
if employee_company_ids:
|
|
managers = db.query(User, UserCompany.company_id).join(UserCompany).filter(
|
|
UserCompany.company_id.in_(employee_company_ids),
|
|
UserCompany.role == 'MANAGER'
|
|
).all()
|
|
for mgr, cid in managers:
|
|
company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''})
|
|
|
|
# Onboarding progress widget
|
|
from utils.onboarding import compute_onboarding_steps
|
|
onboarding = compute_onboarding_steps(current_user, user_companies, current_app.root_path)
|
|
|
|
# Widget 1: Upcoming events (3 nearest future events)
|
|
upcoming_events = db.query(NordaEvent).filter(
|
|
NordaEvent.event_date >= date.today()
|
|
).order_by(NordaEvent.event_date.asc()).limit(3).all()
|
|
|
|
# Batch RSVP lookup for current user + attendee counts
|
|
user_event_ids = set()
|
|
event_attendee_counts = {}
|
|
if upcoming_events:
|
|
event_ids = [e.id for e in upcoming_events]
|
|
rsvps = db.query(EventAttendee.event_id).filter(
|
|
EventAttendee.event_id.in_(event_ids),
|
|
EventAttendee.user_id == current_user.id,
|
|
EventAttendee.status == 'confirmed'
|
|
).all()
|
|
user_event_ids = {r[0] for r in rsvps}
|
|
|
|
# Count confirmed attendees + guests per event
|
|
from sqlalchemy import func
|
|
counts = db.query(EventAttendee.event_id, func.count(EventAttendee.id)).filter(
|
|
EventAttendee.event_id.in_(event_ids),
|
|
EventAttendee.status == 'confirmed'
|
|
).group_by(EventAttendee.event_id).all()
|
|
event_attendee_counts = {eid: cnt for eid, cnt in counts}
|
|
guest_counts = db.query(EventGuest.event_id, func.count(EventGuest.id)).filter(
|
|
EventGuest.event_id.in_(event_ids)
|
|
).group_by(EventGuest.event_id).all()
|
|
for eid, cnt in guest_counts:
|
|
event_attendee_counts[eid] = event_attendee_counts.get(eid, 0) + cnt
|
|
|
|
# Widget 2: Recent announcements (3 latest published, pinned first, not expired)
|
|
recent_announcements = db.query(Announcement).filter(
|
|
Announcement.status == 'published',
|
|
or_(Announcement.expires_at == None, Announcement.expires_at > datetime.now())
|
|
).order_by(Announcement.is_pinned.desc(), Announcement.published_at.desc()).limit(3).all()
|
|
|
|
# Widget 3: Recent forum topics (5 latest active)
|
|
recent_forum_topics = db.query(ForumTopic).filter(
|
|
ForumTopic.is_deleted == False
|
|
).order_by(ForumTopic.updated_at.desc()).limit(5).all()
|
|
|
|
# Widget 4: Recent classifieds (3 active, not test, not expired)
|
|
recent_classifieds = db.query(Classified).filter(
|
|
Classified.is_active == True,
|
|
Classified.is_test == False,
|
|
or_(Classified.expires_at == None, Classified.expires_at > datetime.now())
|
|
).order_by(Classified.created_at.desc()).limit(3).all()
|
|
|
|
# Widget 5: New companies (3 newest)
|
|
new_companies = db.query(Company).order_by(
|
|
Company.created_at.desc()
|
|
).limit(3).all()
|
|
|
|
# Admin KPI widget (only computed for admins)
|
|
admin_kpi = None
|
|
if current_user.can_access_admin_panel():
|
|
from sqlalchemy import func as sqlfunc
|
|
from database import UserSession, PageView, SecurityAlert
|
|
from datetime import timedelta as td
|
|
now = datetime.now()
|
|
week_ago = now - td(days=7)
|
|
prev_week = now - td(days=14)
|
|
|
|
# Active members (logged in last 7d)
|
|
active_7d = db.query(sqlfunc.count(sqlfunc.distinct(UserSession.user_id))).filter(
|
|
UserSession.started_at >= week_ago,
|
|
UserSession.user_id.isnot(None),
|
|
UserSession.is_bot == False
|
|
).scalar() or 0
|
|
|
|
# Total registered users
|
|
total_users = db.query(sqlfunc.count(User.id)).filter(User.is_active == True).scalar() or 0
|
|
|
|
# Sessions this week
|
|
sessions_7d = db.query(sqlfunc.count(UserSession.id)).filter(
|
|
UserSession.started_at >= week_ago,
|
|
UserSession.is_bot == False
|
|
).scalar() or 0
|
|
|
|
# Active problems (security alerts + locked accounts)
|
|
active_problems = db.query(sqlfunc.count(SecurityAlert.id)).filter(
|
|
SecurityAlert.created_at >= week_ago
|
|
).scalar() or 0
|
|
|
|
# Never logged in
|
|
never_logged = db.query(sqlfunc.count(User.id)).filter(
|
|
User.is_active == True,
|
|
User.last_login.is_(None)
|
|
).scalar() or 0
|
|
|
|
admin_kpi = {
|
|
'active_users': active_7d,
|
|
'total_users': total_users,
|
|
'active_pct': round(active_7d / total_users * 100) if total_users > 0 else 0,
|
|
'sessions': sessions_7d,
|
|
'problems': active_problems,
|
|
'never_logged': never_logged,
|
|
}
|
|
|
|
return render_template(
|
|
'dashboard.html',
|
|
admin_kpi=admin_kpi,
|
|
conversations=conversations,
|
|
total_conversations=total_conversations,
|
|
total_messages=total_messages,
|
|
has_pending_application=has_pending_application,
|
|
has_draft_application=has_draft_application,
|
|
pending_application=pending_application,
|
|
user_companies=user_companies,
|
|
unread_notifications=unread_notifications,
|
|
upcoming_events_count=upcoming_events_count,
|
|
user_forum_topics_count=user_forum_topics_count,
|
|
upcoming_events=upcoming_events,
|
|
user_event_ids=user_event_ids,
|
|
event_attendee_counts=event_attendee_counts,
|
|
recent_announcements=recent_announcements,
|
|
recent_forum_topics=recent_forum_topics,
|
|
recent_classifieds=recent_classifieds,
|
|
new_companies=new_companies,
|
|
company_managers_map=company_managers_map,
|
|
onboarding=onboarding
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _get_releases():
|
|
"""Return list of all release notes entries."""
|
|
return [
|
|
{
|
|
'version': 'v1.63.0',
|
|
'date': '8 kwietnia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Nowa wiadomość do wybranej osoby': '/wiadomosci',
|
|
'Tworzenie grup rozmów': '/wiadomosci',
|
|
'Zarządzanie członkami grupy': '/wiadomosci',
|
|
},
|
|
'new': [
|
|
'★ <strong>Nowa wiadomość do wybranej osoby</strong> - przycisk „Nowa wiadomość" otwiera okno z wyszukiwarką osób — wystarczy zacząć pisać imię lub nazwisko, by znaleźć odbiorcę',
|
|
'★ <strong>Tworzenie grup rozmów</strong> - przycisk „Nowa grupa" pozwala nadać nazwę, wybrać członków i od razu rozpocząć wspólną rozmowę',
|
|
'<strong>Zarządzanie członkami grupy</strong> - właściciel grupy może dodawać i usuwać osoby, zmieniać nazwę grupy oraz nadawać rolę administratora z poziomu ikony w nagłówku rozmowy',
|
|
'<strong>Rola administratora w grupie</strong> - administrator może dodawać i usuwać członków oraz zmieniać nazwę grupy, nie będąc właścicielem',
|
|
],
|
|
'improve': [
|
|
'<strong>Wgrywanie logo firmy</strong> - przesłane logo jest automatycznie konwertowane do nowoczesnego formatu i od razu widoczne na profilu, bez konieczności odświeżania',
|
|
'<strong>Licznik uczestników wydarzenia</strong> - na stronie głównej i w kalendarzu widoczna jest prawidłowa liczba uczestników, łącznie z zaproszonymi gośćmi',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.62.0',
|
|
'date': '7 kwietnia 2026',
|
|
'badges': ['improve'],
|
|
'links': {},
|
|
'improve': [
|
|
'<strong>Szybkość ładowania portalu</strong> - strony ładują się zauważalnie szybciej dzięki zmniejszeniu rozmiaru o ponad 250 KB i wyeliminowaniu blokujących zasobów zewnętrznych',
|
|
'<strong>Czytelność na telefonie</strong> - poprawiony kontrast kolorów w stopce strony, zgodnie ze standardami dostępności',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.61.0',
|
|
'date': '6 kwietnia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Płatne wydarzenia': '/kalendarz/',
|
|
'Edycja wydarzenia': '/kalendarz/',
|
|
'Zapraszanie współpracowników': '/kalendarz/',
|
|
},
|
|
'new': [
|
|
'★ <strong>Płatne wydarzenia</strong> - organizator może ustawić kwotę za udział w spotkaniu, a przy każdym uczestniku widoczny jest status płatności',
|
|
'<strong>Edycja wydarzenia</strong> - kadra zarządzająca może edytować opis, datę i szczegóły wydarzenia bezpośrednio na jego stronie',
|
|
'<strong>Zapraszanie współpracowników</strong> - przy zapisywaniu się na wydarzenie można od razu wybrać osoby ze swojej firmy i zapisać je jednym kliknięciem',
|
|
],
|
|
'improve': [
|
|
'<strong>Łączna kwota na wydarzeniu</strong> - na stronie wydarzenia widoczna jest suma do zapłaty za siebie i zaproszonych gości',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.60.0',
|
|
'date': '5 kwietnia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Nowa strona główna': '/',
|
|
'Nowi członkowie na stronie głównej': '/',
|
|
},
|
|
'new': [
|
|
'★ <strong>Nowa strona główna</strong> - przebudowany układ z dwukolumnowym widokiem wydarzeń, najnowszym tematem z forum i kafelkiem nowych członków Izby',
|
|
'<strong>Nowi członkowie na stronie głównej</strong> - firmy przyjęte na ostatnim posiedzeniu Rady są widoczne na stronie głównej z odnośnikiem do pełnej listy',
|
|
],
|
|
'improve': [
|
|
'<strong>Godziny w kalendarzu</strong> - subskrypcja kalendarza (ICS) nie wymyśla godziny zakończenia, gdy organizator jej nie podał',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.59.0',
|
|
'date': '4 kwietnia 2026',
|
|
'badges': ['new'],
|
|
'links': {
|
|
'Strona nowych członków': '/nowi-czlonkowie',
|
|
},
|
|
'new': [
|
|
'★ <strong>Strona nowych członków</strong> - lista firm przyjętych na poszczególnych posiedzeniach Rady Izby, z podziałem na daty i odnośnikami do profili',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.58.0',
|
|
'date': '3 kwietnia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Kreator dodawania firmy': '/admin/company/wizard',
|
|
},
|
|
'new': [
|
|
'★ <strong>Kreator dodawania firmy</strong> - pięcioetapowy formularz dla biura Izby: wystarczy wpisać NIP, a system pobierze dane z KRS/CEIDG, znajdzie stronę internetową i podpowie kategorię działalności',
|
|
'<strong>Automatyczne wyszukiwanie strony WWW</strong> - kreator szuka strony internetowej firmy na podstawie nazwy i NIP-u, a znalezione dane można zaakceptować jednym kliknięciem',
|
|
],
|
|
'improve': [
|
|
'<strong>Zbieranie informacji o firmie</strong> - okno z wynikami wyszukiwania ma zawsze widoczne przyciski akceptacji, nawet przy dłuższych opisach',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.57.0',
|
|
'date': '2 kwietnia 2026',
|
|
'badges': ['new', 'fix'],
|
|
'links': {
|
|
'Zarządzanie zespołem firmy': '/firma/edytuj',
|
|
},
|
|
'new': [
|
|
'★ <strong>Zarządzanie zespołem firmy</strong> - kadra zarządzająca może samodzielnie dodawać nowe osoby do firmy, nadawać im role i zarządzać uprawnieniami, bez pomocy administratora portalu',
|
|
'<strong>Zaproszenie nowego pracownika</strong> - wystarczy podać email i imię — system tworzy konto i wysyła zaproszenie z linkiem do ustawienia hasła',
|
|
'<strong>Role w zespole</strong> - trzy poziomy dostępu: Obserwator (podgląd), Pracownik (edycja wybranych sekcji profilu) i Kadra zarządzająca (pełna kontrola)',
|
|
'<strong>Uprawnienia pracownika</strong> - kadra zarządzająca decyduje, które sekcje profilu firmy może edytować każdy pracownik',
|
|
'<strong>Status aktywności</strong> - przy każdej osobie w zespole widać datę ostatniego logowania, a dla nowych osób możliwość ponownego wysłania zaproszenia',
|
|
],
|
|
'fix': [
|
|
'<strong>Wgrywanie logo</strong> - naprawiono błąd, przez który logo nie zapisywało się prawidłowo po przesłaniu pliku',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.56.0',
|
|
'date': '1 kwietnia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Kalendarz wydarzeń': '/kalendarz/',
|
|
},
|
|
'new': [
|
|
'★ <strong>Goście na wydarzeniach</strong> - organizator może zaprosić gości spoza portalu na wydarzenie, podając imię i email — goście pojawiają się na liście uczestników',
|
|
'<strong>Skrót „Moja firma" w menu</strong> - po kliknięciu w swoje imię w górnym pasku widnieje bezpośredni link do profilu Twojej firmy',
|
|
],
|
|
'improve': [
|
|
'<strong>Okno potwierdzenia</strong> - estetyczne okno dialogowe zamiast systemowego komunikatu przeglądarki przy usuwaniu i innych ważnych akcjach',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.55.0',
|
|
'date': '31 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Raport składek': '/admin/fees',
|
|
},
|
|
'new': [
|
|
'★ <strong>Deklaracje członkowskie jako PDF</strong> - każdą deklarację przystąpienia można pobrać jako profesjonalny dokument PDF, gotowy do druku lub archiwizacji',
|
|
'<strong>Śledzenie statusu deklaracji</strong> - wizualny tracker pokazuje, na jakim etapie jest deklaracja (złożona, weryfikacja, zatwierdzona), z datami poszczególnych kroków',
|
|
'<strong>Historia zmian deklaracji</strong> - przy każdej deklaracji widoczna jest pełna historia wydarzeń — kto i kiedy zmienił status',
|
|
'<strong>Struktura firm w raporcie składek</strong> - raport uwzględnia spółki matki i ich marki, z informacją o powiązaniach między firmami',
|
|
],
|
|
'improve': [
|
|
'<strong>Wklejanie zdjęć w wiadomościach</strong> - można wkleić obraz ze schowka bezpośrednio do rozmowy, ze zmiennym rozmiarem podglądu',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.54.0',
|
|
'date': '28 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'NordaGPT': '/chat',
|
|
},
|
|
'new': [
|
|
'★ <strong>NordaGPT rozpoznaje firmy</strong> - asystent automatycznie dopasowuje pytanie do firm z bazy, nawet gdy nazwa jest wpisana z literówką lub skrótem',
|
|
'★ <strong>Odpowiedzi na żywo</strong> - NordaGPT wyświetla odpowiedź słowo po słowie w czasie rzeczywistym, zamiast czekać na całą odpowiedź',
|
|
'<strong>Propozycje kolejnych pytań</strong> - pod każdą odpowiedzią pojawiają się gotowe pytania, które można kliknąć, żeby kontynuować rozmowę',
|
|
'<strong>Personalne powitanie</strong> - NordaGPT zna Twoje imię i firmę, więc odpowiada w kontekście Twojego profilu',
|
|
],
|
|
'improve': [
|
|
'<strong>Dokładniejsze linki do firm</strong> - wszystkie linki do firm w odpowiedziach są sprawdzane z bazą danych, dzięki czemu nie prowadzą do nieistniejących stron',
|
|
'<strong>Szybkość odpowiedzi</strong> - proste pytania (np. godziny otwarcia, adres) są obsługiwane natychmiast, złożone pytania strategiczne dostają więcej czasu na przemyślenie',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.53.0',
|
|
'date': '27 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Wiadomości': '/wiadomosci',
|
|
},
|
|
'new': [
|
|
'★ <strong>Nowy wygląd wiadomości</strong> - rozmowy wyglądają teraz jak w popularnych komunikatorach, ze zdjęciami profilowymi i bąbelkami wiadomości',
|
|
'<strong>Przypinanie wiadomości</strong> - ważne wiadomości można przypiąć w rozmowie, a przypięte treści dostępne są z poziomu górnego paska',
|
|
'<strong>Natychmiastowe wysyłanie</strong> - wiadomość pojawia się w rozmowie od razu po kliknięciu Enter, bez opóźnienia',
|
|
'<strong>Logo firmy</strong> - w panelu edycji firmy można dodać logo, które wyświetla się w katalogu i na profilu',
|
|
'<strong>Mapa witryny</strong> - wyszukiwarki (Google, Bing) widzą wszystkie firmy, wydarzenia i tematy na forum, co poprawia widoczność portalu w internecie',
|
|
],
|
|
'improve': [
|
|
'<strong>Potwierdzenie odczytania</strong> - pod wiadomością widać dokładną datę i godzinę odczytania, a w grupach — listę osób, które przeczytały',
|
|
'<strong>Czytelniejsze linki w wiadomościach</strong> - linki są wyraźnie widoczne zarówno w jasnych, jak i ciemnych bąbelkach',
|
|
'<strong>Aktualności na stronie głównej</strong> - na telefonie widżet „Co nowego" jest bardziej kompaktowy i nie zasłania treści',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.51.0',
|
|
'date': '25 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Kalendarz wydarzeń': '/kalendarz/?view=grid',
|
|
'Subskrypcja kalendarza': '/kalendarz/',
|
|
},
|
|
'new': [
|
|
'★ <strong>Subskrypcja kalendarza</strong> - wydarzenia Izby można dodać do kalendarza w telefonie (iPhone) lub komputerze (Google Calendar), a nowe spotkania będą się pojawiać automatycznie',
|
|
'★ <strong>Zbieranie informacji o firmie</strong> - przycisk wyszukuje dane o firmie w internecie i pokazuje wyniki do akceptacji — sam decydujesz, które informacje dodać do profilu',
|
|
'<strong>Wskaźnik kompletności profilu</strong> - na stronie firmy widać, ile procent profilu jest wypełnione i jakich informacji brakuje',
|
|
'<strong>Powiązania firm w katalogu</strong> - na liście firm widać, które firmy są markami tej samej spółki (np. Scantric i You\'re Welcome należą do Forte Media)',
|
|
],
|
|
'improve': [
|
|
'<strong>Kalendarz na telefonie</strong> - widok miesięczny działa prawidłowo na małych ekranach, dni z wydarzeniami oznaczone kolorami, kliknięcie w dzień pokazuje listę spotkań',
|
|
'<strong>Ważne wydarzenia na czerwono</strong> - kluczowe wydarzenia (jak prezentacja portalu czy spotkanie z Panią Wojewodą) wyróżnione kolorem czerwonym w kalendarzu',
|
|
'<strong>Menu użytkownika na telefonie</strong> - po kliknięciu w swoje imię opcje konta (Mój Panel, Wiadomości, Wyloguj) wyświetlają się jako wygodny panel wysuwany z dołu ekranu',
|
|
'<strong>NordaGPT — informacja o limitach</strong> - w czacie widoczne jest zużycie dziennego limitu oraz zużycie wszystkich użytkowników portalu, z wyjaśnieniem jak działają limity',
|
|
'<strong>Informacje ze stron WWW firm</strong> - sekcja ze słowami kluczowymi ze strony internetowej firmy pokazuje najważniejsze hasła, z możliwością rozwinięcia pełnej listy',
|
|
'<strong>Panel edycji firmy</strong> - na górze panelu edycji widnieje informacja, co możesz samodzielnie zmienić, a które dane są pobierane automatycznie z rejestrów',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.50.0',
|
|
'date': '20 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Wiadomości grupowe': '/wiadomosci/nowa-grupa',
|
|
'Wyszukiwanie w wiadomościach': '/wiadomosci',
|
|
},
|
|
'new': [
|
|
'★ <strong>Wiadomości grupowe</strong> - można utworzyć grupę z dowolną liczbą członków Izby i prowadzić wspólną rozmowę, jak na Teams czy WhatsApp',
|
|
'★ <strong>Wyszukiwanie w wiadomościach</strong> - pole szukania na górze skrzynki filtruje wiadomości po temacie, treści i nadawcy, wyniki pojawiają się na bieżąco',
|
|
'<strong>Status odczytania w grupie</strong> - pod każdą wiadomością widać zdjęcia osób, które ją przeczytały',
|
|
'<strong>Automatyczne odświeżanie czatu</strong> - nowe wiadomości w grupie pojawiają się bez konieczności odświeżania strony',
|
|
'<strong>Usuwanie wiadomości</strong> - twórca wiadomości może ją usunąć zarówno w rozmowach grupowych, jak i prywatnych',
|
|
'<strong>Zarządzanie grupą</strong> - właściciel grupy może dodawać i usuwać członków, nadawać rolę moderatora i usunąć całą grupę',
|
|
],
|
|
'improve': [
|
|
'<strong>Sześć wydarzeń na stronie głównej</strong> - zamiast czterech, widocznych jest sześć najbliższych wydarzeń w kalendarzu',
|
|
'<strong>Filtrowanie wydarzeń</strong> - przyciski "Norda Biznes" i "Zewnętrzne" na stronie głównej zawsze pokazują do sześciu wydarzeń wybranego typu',
|
|
'<strong>Zdjęcia profilowe w wiadomościach</strong> - zamiast inicjałów wyświetla się zdjęcie użytkownika (jeśli je dodał) w skrzynce, wątkach i na czacie grupowym',
|
|
'<strong>Klikalne profile w wiadomościach</strong> - kliknięcie w zdjęcie lub imię nadawcy prowadzi do profilu tej osoby',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.49.0',
|
|
'date': '19 marca 2026',
|
|
'badges': ['new', 'improve', 'security'],
|
|
'links': {
|
|
'Panel składek': '/admin/fees',
|
|
'Kalendarz wydarzeń': '/kalendarz/',
|
|
'Feed RSS — Wydarzenia': '/feed/events.xml',
|
|
},
|
|
'new': [
|
|
'★ <strong>Panel składek członkowskich</strong> - przegląd opłaconych i zaległych składek z podziałem na miesiące, możliwość oznaczania wpłat i wysyłania przypomnień',
|
|
'★ <strong>Wydarzenia zewnętrzne w kalendarzu</strong> - targi, seminaria i szkolenia od organizacji partnerskich (np. Agencja Rozwoju Pomorza) widoczne w kalendarzu z osobnym oznaczeniem i możliwością wyrażenia zainteresowania',
|
|
'<strong>Przypomnienia o składkach</strong> - administrator może wysłać spersonalizowaną wiadomość z kwotą zaległości do wybranych osób w firmie, z podglądem przed wysłaniem',
|
|
'<strong>Raport składek dla Rady Izby</strong> - raport zbiorczy z wykresami pokazujący stan opłacenia składek i statystyki płatności dostępny dla członków Rady',
|
|
'<strong>Edytor tekstu w wiadomościach</strong> - formatowanie pogrubienia, kursywy, list i linków, wklejanie zdjęć bezpośrednio ze schowka',
|
|
'<strong>Kanały RSS</strong> - portal udostępnia trzy kanały z aktualnościami: wydarzenia Izby, ogłoszenia i wiadomości o elektrowni jądrowej',
|
|
'<strong>Filtr wydarzeń w kalendarzu</strong> - jedno kliknięcie pozwala pokazać lub ukryć wydarzenia zewnętrzne',
|
|
],
|
|
'improve': [
|
|
'<strong>Rok założenia firmy na profilu</strong> - właściciel firmy może uzupełnić rok rozpoczęcia działalności w edycji profilu',
|
|
'<strong>NordaGPT zna więcej o firmach</strong> - asystent AI korzysta teraz ze wszystkich pól profilu: rok założenia, liczba pracowników, certyfikaty, realizacje',
|
|
],
|
|
'security': [
|
|
'<strong>Ograniczony dostęp dla gości</strong> - osoby bez aktywnego członkostwa widzą tylko podstawowe informacje o firmach, bez dostępu do osób, wiadomości i forum',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.48.0',
|
|
'date': '18 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Kalendarz': '/kalendarz/',
|
|
},
|
|
'new': [
|
|
'<strong>Załączniki do wydarzeń</strong> - do wydarzeń w kalendarzu można dołączyć pliki, np. agendę czy materiały dla uczestników',
|
|
'<strong>Eksport do kalendarza osobistego</strong> - przycisk "Dodaj do kalendarza" generuje plik ICS z przypomnieniem na dzień przed, lokalizacją i organizatorem',
|
|
'<strong>Panel aktywności użytkowników</strong> - statystyki logowań, najpopularniejsze przeglądarki i urządzenia dostępne dla administratorów',
|
|
],
|
|
'improve': [
|
|
'<strong>Przyciski zapisów na wydarzenia</strong> - zielony przycisk "Zapisany" z możliwością rezygnacji po najechaniu kursorem',
|
|
'<strong>Rozpoznawanie osób w opisach wydarzeń</strong> - imiona i nazwiska członków Izby wspomniane w opisie wydarzenia stają się linkami do ich profili',
|
|
'<strong>Edycja profilu firmy</strong> - rok założenia, rozbudowany edytor opisu z formatowaniem tekstu',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.47.0',
|
|
'date': '17 marca 2026',
|
|
'badges': ['improve'],
|
|
'links': {
|
|
'Kalendarz': '/kalendarz/',
|
|
},
|
|
'improve': [
|
|
'<strong>Klikalne osoby w kalendarzu</strong> - wszyscy zapisani uczestnicy wydarzeń wyświetlani jako kolorowe "plakietki" z linkiem do profilu',
|
|
'<strong>Ulepszone szczegóły wydarzeń</strong> - nowy wygląd strony wydarzenia z logo firmy prelegenta i listą zapisanych osób',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.46.0',
|
|
'date': '16 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Sekcja PEJ — Elektrownia Jądrowa': '/pej',
|
|
'Local Content': '/pej/local-content',
|
|
'Aktualności PEJ': '/pej/aktualnosci',
|
|
'Kontakty zewnętrzne': '/kontakty',
|
|
},
|
|
'new': [
|
|
'★ <strong>Sekcja PEJ — Elektrownia Jądrowa</strong> - nowa przestrzeń poświęcona projektowi budowy pierwszej elektrowni jądrowej w Polsce, z aktualnościami, listą firm gotowych do współpracy i informacjami dla dostawców',
|
|
'★ <strong>Local Content</strong> - pełna lista firm z Izby dopasowanych do projektu PEJ z możliwością filtrowania po branży i typie współpracy',
|
|
'<strong>Multimedia PEJ</strong> - filmy o elektrowni jądrowej i infografiki ze schematem bloku oraz harmonogramem budowy',
|
|
'<strong>Źródła i kontakty PEJ</strong> - dane kontaktowe do kluczowych osób w PEJ i Bechtel, linki do platformy zakupowej i rejestracji dostawców',
|
|
'<strong>Śledzenie przeczytanych artykułów</strong> - kliknięty artykuł w aktualnościach PEJ zostaje oznaczony, dzięki czemu widać co już było przeczytane',
|
|
],
|
|
'improve': [
|
|
'<strong>Menu nawigacji</strong> - "Kaszubia/PEJ" w jednym miejscu zamiast rozrzuconych linków, mniej pozycji w menu dla lepszej przejrzystości',
|
|
'<strong>66 aktualności nuklearnych</strong> - istniejące wiadomości o elektrowni jądrowej zostały automatycznie przypisane do sekcji PEJ',
|
|
'<strong>Kontakty zewnętrzne</strong> - dodano 8 nowych kontaktów związanych z projektem PEJ: osoby w PEJ, Bechtel, PAA i Ministerstwie Przemysłu',
|
|
'<strong>Roadmapa Kaszubia</strong> - zaktualizowana oś czasu z 8 zweryfikowanymi kamieniami milowymi: Baltic Power, elektrownia jądrowa, Kongsberg i offshore',
|
|
'<strong>Strona Kaszubia</strong> - uporządkowana treść, usunięte elementy techniczne, czytelniejszy układ dla członków Izby',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.45.0',
|
|
'date': '15 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Projekty na profilu firmy': '/company/rotor',
|
|
},
|
|
'new': [
|
|
'<strong>Monitoring dostępności portalu</strong> - panel pokazujący czas działania serwisu w ostatnich 24 godzinach i 30 dniach',
|
|
'<strong>Statystyki aktywności użytkowników</strong> - portal śledzi liczbę logowań i ostatnią aktywność, co pozwala lepiej rozumieć jak członkowie korzystają z platformy',
|
|
],
|
|
'improve': [
|
|
'<strong>Projekty na profilu firmy</strong> - na stronie każdej firmy widać do jakich projektów ZOP Kaszubia została dopasowana',
|
|
'<strong>Ciekawostki na stronie głównej</strong> - widżet "Czy wiesz, że..." pokazuje najnowsze fakty z bazy wiedzy o projektach na Kaszubach z możliwością załadowania kolejnych',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.44.0',
|
|
'date': '13 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Poradnik Google Reviews': '/edukacja/opinie-google',
|
|
},
|
|
'new': [
|
|
'<strong>Poradnik Google Reviews</strong> - w sekcji Edukacja nowy materiał o tym, jak zbierać opinie klientów w Google i dlaczego to ważne dla widoczności firmy',
|
|
'<strong>Resetowanie hasła przez admina</strong> - administrator może wysłać użytkownikowi link do zmiany hasła bez znajomości obecnego hasła',
|
|
],
|
|
'improve': [
|
|
'<strong>Grupowy audyt wizytówek Google</strong> - możliwość sprawdzenia wielu wizytówek jednocześnie z podglądem wyników przed zapisem',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.43.0',
|
|
'date': '12 marca 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'links': {
|
|
'Zdjęcie profilowe': '/konto',
|
|
'Profile użytkowników': '/kalendarz',
|
|
'Klikalne lokalizacje': '/kalendarz',
|
|
'Lista wydarzeń po polsku': '/kalendarz',
|
|
},
|
|
'new': [
|
|
'<strong>Nowości na stronie głównej</strong> - pod nagłówkiem katalogu firm wyświetla się informacja o najnowszych zmianach na platformie z linkiem do pełnej listy',
|
|
'<strong>Polityka prywatności i Regulamin</strong> - platforma ma teraz własne dokumenty prawne dostępne w stopce strony',
|
|
'★ <strong>Zdjęcie profilowe</strong> - każdy zalogowany użytkownik może dodać swoje zdjęcie, które jest automatycznie przycinane i optymalizowane',
|
|
'★ <strong>Profile użytkowników</strong> - po kliknięciu w imię uczestnika wydarzenia lub autora na forum otwiera się strona z jego aktywnością, firmą i możliwością wysłania wiadomości',
|
|
'<strong>Klikalne lokalizacje</strong> - adresy wydarzeń w kalendarzu prowadzą teraz bezpośrednio do Google Maps',
|
|
'<strong>Klikalne nazwiska</strong> - imiona i nazwiska członków Izby wymienione w opisach wydarzeń prowadzą do ich profili',
|
|
'<strong>Automatyczne linkowanie adresów</strong> - adresy stron internetowych w opisach wydarzeń stają się klikalnymi linkami',
|
|
'<strong>Przyciski kontaktowe na profilu</strong> - na stronie osoby widoczne są przyciski do wysłania wiadomości, sprawdzenia firmy i kontaktu',
|
|
],
|
|
'improve': [
|
|
'<strong>Strona wydarzenia</strong> - nowy wygląd z logo firmy prelegenta, możliwością dodania do kalendarza i klikalnymi nazwiskami uczestników',
|
|
'<strong>Lista wydarzeń po polsku</strong> - kalendarz wyświetla polskie nazwy miesięcy, dni tygodnia i przycisk zapisania się na wydarzenie',
|
|
'<strong>Wzbogacone karty osób</strong> - profil osoby pokazuje teraz odznakę członka Rady, ostatnią aktywność, liczbę wydarzeń i wpisów na forum',
|
|
'<strong>Automatyczne łączenie kont</strong> - po zalogowaniu portal sam rozpoznaje powiązanie użytkownika z osobą w rejestrze KRS',
|
|
],
|
|
'fix': [
|
|
'<strong>Formatowanie opisów wydarzeń</strong> - opisy wpisane jako zwykły tekst wyświetlają się teraz z prawidłowymi akapitami i odstępami',
|
|
'<strong>Strona błędu</strong> - zamiast smutnej buźki wyświetla się logo Norda Biznes',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.42.0',
|
|
'date': '11 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Załączniki do wiadomości': '/wiadomosci/nowa',
|
|
'Wyszukiwanie odbiorcy': '/wiadomosci/nowa',
|
|
'Historia korespondencji': '/wiadomosci',
|
|
},
|
|
'new': [
|
|
'★ <strong>Załączniki do wiadomości</strong> - do każdej wiadomości prywatnej można dołączyć pliki (dokumenty, zdjęcia, PDF) zarówno przy wysyłaniu nowej wiadomości, jak i przy odpowiedzi',
|
|
'★ <strong>Wyszukiwanie odbiorcy</strong> - zamiast rozwijanej listy wystarczy zacząć wpisywać imię, nazwisko lub nazwę firmy, a portal podpowie pasujące osoby',
|
|
'<strong>Podgląd profilu odbiorcy</strong> - po wybraniu odbiorcy wyświetla się karta z jego danymi i firmą, dzięki czemu wiadomo do kogo trafia wiadomość',
|
|
'<strong>Potwierdzenie odczytania</strong> - w skrzynce wysłanych widoczne jest, czy odbiorca przeczytał wiadomość',
|
|
'<strong>Historia korespondencji</strong> - po otwarciu wiadomości widoczny jest cały wątek rozmowy z daną osobą w jednym miejscu',
|
|
],
|
|
'improve': [
|
|
'<strong>Kolorowe statusy wiadomości</strong> - wiadomości nowe, wysłane i przeczytane oznaczone są kolorowymi etykietami dla łatwiejszego przeglądania',
|
|
'<strong>Nowy wygląd skrzynki</strong> - skrzynka odbiorcza i wysłane zostały przebudowane dla lepszej czytelności na komputerze i telefonie',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.41.0',
|
|
'date': '7 marca 2026',
|
|
'badges': ['new', 'improve'],
|
|
'links': {
|
|
'Oznaczenie „Nowe"': '/pulpit',
|
|
},
|
|
'new': [
|
|
'<strong>Oznaczenie „Nowe"</strong> - na pulpicie widoczne jest, które wydarzenia i ogłoszenia pojawiły się od ostatniego logowania',
|
|
'<strong>Oznaczenia dostępu do wydarzeń</strong> - wydarzenia z ograniczonym dostępem (tylko dla Rady lub ukryte) mają widoczne etykiety',
|
|
],
|
|
'improve': [
|
|
'<strong>Więcej wydarzeń na stronie głównej</strong> - zamiast dwóch wyświetlają się cztery najbliższe wydarzenia w przejrzystej siatce',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.40.0',
|
|
'date': '7 marca 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'links': {
|
|
'Sekcja Aktualności': '/ogloszenia',
|
|
'Ulepszona sztuczna inteligencja': '/chat',
|
|
},
|
|
'new': [
|
|
'<strong>Sekcja Aktualności</strong> - ogłoszenia i informacje dla członków Izby z wyróżnionymi wydarzeniami, zdjęciami i załącznikami',
|
|
'<strong>Ulepszona sztuczna inteligencja</strong> - NordaGPT korzysta teraz z najnowszej generacji modeli Google Gemini 3.1 z lepszym rozumieniem pytań',
|
|
],
|
|
'improve': [
|
|
'<strong>Szybsze audyty SEO</strong> - analiza stron internetowych firm korzysta z wydajniejszego parsera HTML',
|
|
'<strong>Płynniejsze korzystanie z portalu</strong> - automatyczne odświeżanie powiadomień i wiadomości nie wpływa już na limit zapytań',
|
|
],
|
|
'fix': [
|
|
'<strong>Kalendarz</strong> - naprawiono błąd przy wpisaniu nieprawidłowego roku w adresie strony',
|
|
'<strong>Audyt SEO</strong> - naprawiono zapis danych o wykorzystaniu zapytań do Google PageSpeed',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.39.0',
|
|
'date': '23 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'links': {
|
|
'Zarządzanie powiadomieniami': '/konto/prywatnosc',
|
|
},
|
|
'new': [
|
|
'★ <strong>Powiadomienia e-mail o wiadomościach</strong> - po otrzymaniu wiadomości prywatnej przychodzi e-mail z podglądem treści i linkiem do odpowiedzi',
|
|
'<strong>Zarządzanie powiadomieniami</strong> - w ustawieniach prywatności można włączyć lub wyłączyć powiadomienia e-mail o nowych wiadomościach',
|
|
'★ <strong>Instalacja aplikacji na telefonie</strong> - na Androidzie wystarczy jedno kliknięcie „Zainstaluj", na iPhonie instrukcja krok po kroku z obrazkami',
|
|
],
|
|
'improve': [
|
|
'<strong>Klikalne linki na forum</strong> - adresy stron internetowych wklejone we wpisach na forum automatycznie stają się klikalnymi linkami',
|
|
'<strong>Godziny otwarcia na żywo</strong> - status „otwarte/zamknięte" na profilu firmy obliczany jest na bieżąco na podstawie aktualnej godziny',
|
|
],
|
|
'fix': [
|
|
'<strong>Formatowanie wpisów na forum</strong> - poprawiono zbyt duże odstępy między wierszami i prawidłowe wyświetlanie list',
|
|
'<strong>Podwójne wysyłanie odpowiedzi</strong> - naprawiono błąd umożliwiający przypadkowe podwójne wysłanie odpowiedzi na forum',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.38.0',
|
|
'date': '22 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'links': {
|
|
'Osoby kontaktowe na profilu firmy': '/',
|
|
},
|
|
'new': [
|
|
'★ <strong>Osoby kontaktowe na profilu firmy</strong> - na profilu każdej firmy widoczne są osoby powiązane z firmą wraz z danymi kontaktowymi i możliwością wysłania wiadomości',
|
|
'<strong>Wiadomości w menu głównym</strong> - nowy skrót do wiadomości prywatnych w górnym menu z licznikiem nieprzeczytanych',
|
|
'<strong>Powiadomienia o nowych wiadomościach</strong> - po otrzymaniu wiadomości prywatnej pojawia się powiadomienie w dzwoneczku',
|
|
],
|
|
'improve': [
|
|
'<strong>Karta kontaktowa na profilu firmy</strong> - wyraźne przyciski do wysłania e-maila i wiadomości prywatnej na portalu',
|
|
'<strong>Oznaczenie członków Norda</strong> - osoby powiązane z firmami członkowskimi mają widoczny znaczek „Norda"',
|
|
'<strong>Podgląd reakcji na forum</strong> - po najechaniu na reakcję widoczna lista osób, które zareagowały',
|
|
],
|
|
'fix': [
|
|
'<strong>Podgląd profilu na forum</strong> - naprawiono wyświetlanie statystyk po najechaniu na autora wpisu',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.37.0',
|
|
'date': '21 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'★ <strong>Wyszukiwanie stron internetowych firm</strong> - automatyczne znajdowanie stron WWW dla firm, które ich nie mają w katalogu',
|
|
'<strong>Audyt SEO portalu</strong> - panel do sprawdzania jakości SEO samej platformy Norda Biznes z historią wyników',
|
|
'<strong>Firmy na stronie głównej</strong> - kafelki firm członkowskich widoczne na stronie startowej portalu',
|
|
'<strong>Nowy wygląd e-maili</strong> - ujednolicony, profesjonalny szablon wszystkich wiadomości e-mail z portalu',
|
|
],
|
|
'improve': [
|
|
'<strong>Jakość danych firm</strong> - nowy panel z oceną kompletności danych, podpowiedziami i hurtowym uzupełnianiem informacji',
|
|
'<strong>Poprawne adresy stron firm</strong> - linki do stron internetowych działają teraz prawidłowo we wszystkich przypadkach',
|
|
],
|
|
'fix': [
|
|
'<strong>Poprawny adres i telefon Izby</strong> - zaktualizowano dane kontaktowe na stronie portalu',
|
|
'<strong>Linki do Facebooka</strong> - naprawiono wyświetlanie odnośnika do strony Norda Biznes na Facebooku',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.36.0',
|
|
'date': '20 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'★ <strong>Wykresy analityki postów</strong> - 6 wykresów pokazujących zaangażowanie, aktywność publikacji, typy postów i najlepsze dni/godziny',
|
|
'<strong>Ranking Top 5 postów</strong> - najlepiej performujące posty z Facebooka z linkami do oryginału',
|
|
'<strong>Paginacja listy postów</strong> - wybór wyświetlania 10, 20, 50, 100 lub wszystkich postów na stronie',
|
|
'<strong>Zwijane sekcje</strong> - każdą sekcję można zwinąć lub rozwinąć, a ustawienie jest zapamiętywane między wizytami',
|
|
'<strong>Przycisk "Zaktualizuj dane"</strong> - ponowne pobranie wszystkich danych firmy z rejestrów, audytów i mediów społecznościowych',
|
|
],
|
|
'improve': [
|
|
'<strong>Audyt wizytówki Google</strong> - czytelniejszy widok wyników i weryfikacja czy znaleziona wizytówka należy do szukanej firmy',
|
|
'<strong>Wykrywanie profili społecznościowych</strong> - naprawiono zapisywanie znalezionych profili Facebook, Instagram, YouTube, LinkedIn i TikTok',
|
|
'<strong>Pobieranie danych z CEIDG</strong> - poprawiono wyszukiwanie jednoosobowych firm w rejestrze CEIDG z pełnym mapowaniem pól',
|
|
'<strong>Automatyczne wykresy</strong> - wykresy wyświetlają się natychmiast po wejściu na stronę bez konieczności klikania',
|
|
'<strong>Nowa kolejność sekcji</strong> - wykresy na górze, posty z aplikacji w środku, posty z Facebooka na dole',
|
|
],
|
|
'fix': [
|
|
'<strong>Logo firm w formacie SVG</strong> - naprawiono wyświetlanie logo zapisanych w formacie wektorowym',
|
|
'<strong>Pobieranie logo</strong> - naprawiono błąd uniemożliwiający pobranie logo z niektórych stron internetowych',
|
|
'<strong>Duplikaty postów z Facebooka</strong> - naprawiono problem z pobieraniem tych samych postów wielokrotnie',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.35.0',
|
|
'date': '19 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'★ <strong>Moduł Social Media</strong> - nowy moduł do zarządzania obecnością firmy w mediach społecznościowych',
|
|
'<strong>Tworzenie postów z pomocą AI</strong> - generowanie treści i hashtagów przez sztuczną inteligencję',
|
|
'<strong>Wybór tonu komunikacji</strong> - 5 stylów pisania od autentycznego przez ekspercki po inspirujący',
|
|
'<strong>Wybór silnika AI</strong> - możliwość wyboru modelu do generowania treści',
|
|
'<strong>Publikowanie na Facebooku</strong> - tworzenie i publikowanie postów bezpośrednio z platformy',
|
|
'<strong>Widoczność postów</strong> - przełączanie między trybem testowym a publicznym dla każdego posta',
|
|
'<strong>Posty z Facebooka ze statystykami</strong> - wyświetlanie polubień, komentarzy, udostępnień i reakcji',
|
|
'<strong>Diagram cyklu życia posta</strong> - wizualizacja procesu od szkicu po publikację',
|
|
'<strong>Synchronizacja danych z Facebooka</strong> - automatyczne pobieranie kategorii, telefonu i adresu ze strony firmowej',
|
|
'<strong>Integracje dla kierowników</strong> - strona integracji dostępna dla osób z rolą Zarządzający',
|
|
],
|
|
'improve': [
|
|
'<strong>Linki social media w postach AI</strong> - generowane treści uwzględniają profile firmy w mediach społecznościowych',
|
|
'<strong>Monitorowanie kosztów AI</strong> - informacja o zużyciu zasobów przy generowaniu treści',
|
|
'<strong>Liczba uczestników przy wydarzeniach</strong> - widoczna na pulpicie obok nadchodzących wydarzeń',
|
|
'<strong>Klikalna karta "Twoja firma"</strong> - na pulpicie prowadzi do profilu firmy',
|
|
'<strong>Ochrona adresów Facebook</strong> - identyfikator numeryczny nie nadpisuje czytelnego adresu firmy',
|
|
],
|
|
'fix': [
|
|
'<strong>Połączenie z Facebookiem</strong> - naprawiono przechowywanie danych autoryzacji dla niezawodnego publikowania',
|
|
'<strong>Ponowna próba publikacji</strong> - nieudane posty można opublikować ponownie',
|
|
'<strong>Diagnostyka połączenia z Facebookiem</strong> - dodano informacje pomagające rozwiązać problemy z połączeniem',
|
|
'<strong>Opisy wydarzeń w kalendarzu</strong> - poprawiono wyświetlanie formatowanego tekstu w widoku listy',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.34.0',
|
|
'date': '18 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>Edytor z podglądem na żywo</strong> - edycja opisów firmy z formatowaniem tekstu i natychmiastowym podglądem wyniku',
|
|
'★ <strong>Automatyczne pobieranie logo</strong> - system pobiera logo ze strony internetowej firmy i proponuje kandydatów z oceną jakości',
|
|
'<strong>Galeria kandydatów na logo</strong> - porównanie wielu propozycji logo przed wyborem najlepszego',
|
|
'<strong>Notatki administracyjne o firmach</strong> - wewnętrzne notatki widoczne tylko dla administratorów',
|
|
'<strong>Kontrola ról nowych użytkowników</strong> - nowi użytkownicy otrzymują ograniczoną rolę do czasu zatwierdzenia przez admina',
|
|
'<strong>Zmiana roli użytkownika z listy</strong> - administrator może zmieniać rolę bezpośrednio w panelu użytkowników',
|
|
],
|
|
'improve': [
|
|
'<strong>Nowy wygląd strony edycji firmy</strong> - czytelniejszy układ z kolorową organizacją sekcji',
|
|
'<strong>Porównanie logo przed zmianą</strong> - podgląd dotychczasowego i nowego logo obok siebie',
|
|
'<strong>Uprawnienia kierowników</strong> - osoby zarządzające automatycznie otrzymują pełne uprawnienia przy tworzeniu konta',
|
|
'<strong>Powiadomienie o nowej rejestracji</strong> - naprawiono wysyłanie emaila z alertem o oczekujących zatwierdzeniach',
|
|
],
|
|
'fix': [
|
|
'<strong>Pobieranie danych z CEIDG</strong> - naprawiono pobieranie i mapowanie pól z rejestru jednoosobowych firm',
|
|
'<strong>Audyt SEO</strong> - poprawna obsługa wolnych połączeń i stron, które nie istnieją',
|
|
'<strong>Wyświetlanie formatowanych opisów firm</strong> - opisy z edytora wyświetlają się poprawnie na profilu firmy',
|
|
'<strong>Audyt social media</strong> - usunięto źródło generujące zbyt wiele fałszywych wyników',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.33.0',
|
|
'date': '17 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>Wiele stron internetowych na profilu firmy</strong> - możliwość dodania do 5 stron z oznaczeniem typu (główna, e-commerce, blog, portfolio)',
|
|
'<strong>Kolorowe przyciski typów stron</strong> - każdy typ strony ma inny kolor z podpowiedzią po najechaniu',
|
|
'<strong>Osobne wyniki audytu dla każdej strony</strong> - karta wyników SEO wyświetlana oddzielnie dla każdej strony firmy',
|
|
'<strong>Kontrola widoczności sekcji profilu</strong> - właściciel firmy może ukrywać lub pokazywać poszczególne sekcje (rejestry, social media, SEO)',
|
|
'<strong>Szczegółowa kontrola sekcji rejestrów</strong> - oddzielne przełączniki dla KRS, CEIDG i Białej Listy VAT',
|
|
'<strong>Rozpoznawanie typu profilu LinkedIn</strong> - audyt rozróżnia profil firmowy od osobistego',
|
|
],
|
|
'improve': [
|
|
'<strong>Dokładniejsze wykrywanie LinkedIn</strong> - lepsze rozpoznawanie profili firmowych w wynikach wyszukiwania',
|
|
'<strong>Pasek postępu audytu social media</strong> - dodatkowy krok wyszukiwania widoczny w pasku postępu',
|
|
],
|
|
'fix': [
|
|
'<strong>Usuwanie stron internetowych firmy</strong> - naprawiono usuwanie wszystkich stron przy zapisywaniu zmian',
|
|
'<strong>Rozdzielanie połączonych adresów</strong> - system prawidłowo rozdziela adresy zapisane w jednym polu',
|
|
'<strong>Wykrywanie LinkedIn</strong> - poprawiono dokładność sprawdzania profili LinkedIn',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.32.0',
|
|
'date': '14 lutego 2026',
|
|
'badges': ['improve', 'fix'],
|
|
'improve': [
|
|
'<strong>Powiadomienia na telefonie</strong> - dropdown wyświetla się jako panel od dołu ekranu zamiast być obcinanym',
|
|
'<strong>Dodawanie zdjęć na forum z telefonu</strong> - przycisk "Dodaj zdjęcie z galerii" zamiast "Ctrl+V" na urządzeniach mobilnych',
|
|
],
|
|
'fix': [
|
|
'<strong>Usuwanie odpowiedzi na forum</strong> - naprawiono błąd uniemożliwiający usunięcie odpowiedzi, które były już przeczytane przez innych',
|
|
'<strong>Wysyłanie załączników z telefonu</strong> - naprawiono problem z nieprzesyłaniem zdjęć wybranych z galerii na urządzeniach mobilnych',
|
|
'<strong>Pulpit użytkownika</strong> - naprawiono błąd 500 przy wyświetlaniu dashboardu',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.31.0',
|
|
'date': '13 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>Widget postępu wdrożenia na pulpicie</strong> - widoczny stan konfiguracji firmy po zalogowaniu',
|
|
'<strong>Zdjęcia w wydarzeniach</strong> - wydarzenia mogą zawierać zdjęcia w opisie',
|
|
'<strong>Nowa firma: UNIMOT Energia i Gaz</strong> - logotyp nowej firmy członkowskiej',
|
|
'<strong>Weryfikacja certyfikatu SSL w audycie SEO</strong> - sprawdzanie ważności certyfikatu strony',
|
|
],
|
|
'improve': [
|
|
'<strong>Czytelniejsze opisy wydarzeń</strong> - poprawione formatowanie tekstu i wyświetlanie obrazów w kalendarzu',
|
|
'<strong>Walidacja NIP przy rejestracji</strong> - sprawdzanie sumy kontrolnej numeru NIP',
|
|
],
|
|
'fix': [
|
|
'<strong>Audyt SEO</strong> - naprawiono ładowanie kluczy API przy uruchamianiu audytu',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.30.0',
|
|
'date': '12 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>3 najbliższe wydarzenia na stronie głównej</strong> - szybki podgląd kalendarza bez wchodzenia w szczegóły',
|
|
'<strong>Nowe firmy: Alter Energy, Fiume Studio</strong> - logotypy nowych firm członkowskich',
|
|
'<strong>Marki zależne dziedziczą NIP</strong> - marki podrzędne automatycznie wyświetlają NIP firmy macierzystej',
|
|
],
|
|
'improve': [
|
|
'<strong>Lepsza detekcja profili Facebook w audycie</strong> - wykluczenie pixeli śledzących i parametrów Meta z wyników',
|
|
],
|
|
'fix': [
|
|
'<strong>Synchronizacja użytkownik-firma</strong> - naprawiono powiązania w tabeli user_companies i nazwy pól kalendarza',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.29.0',
|
|
'date': '9 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>Analiza planu rozwoju Izby przez AI</strong> - sztuczna inteligencja ocenia postęp realizacji strategii i wskazuje braki',
|
|
'<strong>Panel wiedzy ZOPK na stronie głównej</strong> - wybrane wpisy bazy wiedzy widoczne bez wchodzenia w sekcję ZOPK',
|
|
'<strong>Przyciski zarządzania na stronach ZOPK</strong> - administratorzy mogą edytować treści bezpośrednio ze strony publicznej',
|
|
'<strong>Baza wiedzy ZOPK w czasie rzeczywistym</strong> - postęp wyszukiwania i ekstrakcji widoczny na żywo w przeglądarce',
|
|
'<strong>4-etapowy panel zarządzania bazą wiedzy ZOPK</strong> - workflow od wyszukiwania przez scraping po ekstrakcję',
|
|
'<strong>Rozszerzenie Google API do ~100%</strong> - GBP Performance i Google Search Console w pełni zintegrowane',
|
|
'<strong>Kontakt z przełożonym dla pracowników</strong> - pracownik widzi dane kontaktowe osoby zarządzającej firmą',
|
|
'<strong>Pełna matryca uprawnień w panelu dostępu</strong> - widoczne wszystkie uprawnienia dla każdej roli',
|
|
'<strong>Bezpośrednie ustawianie hasła przez admina</strong> - bez konieczności wysyłania linku resetującego',
|
|
'<strong>Kolumna ostatniego logowania</strong> w liście użytkowników',
|
|
],
|
|
'improve': [
|
|
'<strong>Automatyczne odświeżanie tokenów OAuth</strong> - strona integracji nie wymaga ponownego logowania po wygaśnięciu tokenu',
|
|
'<strong>Stylizowane okno potwierdzenia rozłączenia OAuth</strong> - zamiast standardowego okna przeglądarki',
|
|
'<strong>Wydzielone przyciski audytu na profilu firmy</strong> - przeniesione do osobnego wiersza dla lepszej czytelności',
|
|
'<strong>Link "Mój pulpit" na profilu firmy</strong> - szybki powrót do panelu użytkownika',
|
|
'<strong>Dostęp do audytów ograniczony do wyznaczonego użytkownika</strong>',
|
|
'<strong>Menu uproszczone dla zwykłych użytkowników</strong> - ukrycie opcji widocznych tylko dla właściciela platformy',
|
|
],
|
|
'fix': [
|
|
'<strong>Polskie znaki w audytach</strong> - naprawiono wyświetlanie polskich znaków diakrytycznych w szablonach audytu',
|
|
'<strong>Baza wiedzy ZOPK</strong> - naprawiono model embeddingów, scraper treści i polskie komunikaty błędów',
|
|
'<strong>Pulpit użytkownika</strong> - naprawiono błąd wylogowywania przy wyświetlaniu dashboardu z powiązanymi firmami',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.28.0',
|
|
'date': '8 lutego 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'★ <strong>Audyt SEO: dane z Google Search Console</strong> - kliknięcia, wyświetlenia i średnia pozycja z Google',
|
|
'<strong>Audyt SEO: dane CrUX</strong> - rzeczywiste pomiary szybkości od użytkowników Chrome',
|
|
'<strong>Audyt SEO: nagłówki bezpieczeństwa</strong> - sprawdzanie konfiguracji HTTPS i nagłówków ochronnych',
|
|
'<strong>Audyt SEO: formaty obrazów</strong> - analiza wykorzystania nowoczesnych formatów WebP/AVIF',
|
|
'<strong>Audyt SEO: pasek postępu i podsumowanie wyników</strong> - czytelna wizualizacja stanu strony',
|
|
'<strong>Audyt SEO: instrukcje konfiguracji Search Console</strong> - krok po kroku dla firm bez połączonego konta',
|
|
'<strong>Audyt GBP: migracja na Places API (New)</strong> - nowsze i dokładniejsze dane o firmach w Google Maps',
|
|
'<strong>Audyt GBP: link do recenzji, wskazówki dojazdu, status otwarcia</strong>',
|
|
'<strong>Audyt AI: analiza sentymentu recenzji i benchmarki branżowe</strong>',
|
|
'<strong>Integracje OAuth</strong> - framework do łączenia kont Google i Meta z platformą',
|
|
],
|
|
'improve': [
|
|
'<strong>Migracja FID na INP</strong> - nowy standard Google do pomiaru interaktywności strony',
|
|
'<strong>Rozszerzone wyświetlanie danych GBP i Social Media</strong> w panelach audytu',
|
|
],
|
|
'fix': [
|
|
'<strong>Audyt GBP</strong> - naprawiono fałszywe wykrywanie zdjęć i logo',
|
|
'<strong>Audyt SEO</strong> - naprawiono zamianę klucza fid_ms na inp_ms w trasie',
|
|
'<strong>Search Console</strong> - obsługa formatu domain property (sc-domain:)',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.27.0',
|
|
'date': '6 lutego 2026',
|
|
'badges': ['security', 'new', 'improve', 'fix'],
|
|
'links': {
|
|
'Edycja profilu firmy przez właściciela': '/pulpit',
|
|
'Pobieranie danych z rejestrów urzędowych': '/',
|
|
'NordaGPT zna Izbę NORDA': '/chat',
|
|
'Sekcja "Co nowego w Izbie?" na pulpicie': '/pulpit',
|
|
},
|
|
'security': [
|
|
'<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'],
|
|
'links': {
|
|
'Strefa RADA': '/rada',
|
|
'Korzyści dla Członków': '/korzysci',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Składanie wniosków o członkostwo': '/rejestracja',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Tablica B2B: Przycisk "Jestem zainteresowany"': '/b2b',
|
|
'Forum: Reakcje emoji': '/forum',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Moje konto': '/konto',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Aplikacja mobilna': '/',
|
|
'NordaGPT: Nowy silnik AI': '/chat',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Ukrywanie telefonu i email': '/konto/prywatnosc',
|
|
'Blokowanie użytkowników': '/konto/blokady',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Sekcja Aktualności': '/ogloszenia',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'NordaGPT zna więcej danych': '/chat',
|
|
'Kalendarz: Widok miesięczny': '/kalendarz',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Mapa Powiązań': '/mapa-powiazan',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Kalendarz wydarzeń': '/kalendarz',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'Chatbot NordaGPT': '/chat',
|
|
'Wyszukiwarka firm': '/search',
|
|
},
|
|
'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'],
|
|
'links': {
|
|
'System wiadomości prywatnych': '/wiadomosci',
|
|
},
|
|
'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',
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
@bp.route('/release-notes')
|
|
def release_notes():
|
|
"""Historia zmian platformy."""
|
|
releases = _get_releases()
|
|
|
|
# Statystyki (używa globalnej stałej COMPANY_COUNT_MARKETING)
|
|
db = SessionLocal()
|
|
try:
|
|
stats = {
|
|
'companies': COMPANY_COUNT_MARKETING,
|
|
'categories': db.query(Category).filter(Category.parent_id.isnot(None)).count(),
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
return render_template('release_notes.html', releases=releases, stats=stats)
|
|
|
|
|
|
@bp.route('/polityka-prywatnosci')
|
|
def polityka_prywatnosci():
|
|
"""Polityka prywatności platformy."""
|
|
return render_template('polityka_prywatnosci.html')
|
|
|
|
|
|
@bp.route('/regulamin')
|
|
def regulamin():
|
|
"""Regulamin platformy."""
|
|
return render_template('regulamin.html')
|
|
|
|
|
|
@bp.route('/zainstaluj-aplikacje')
|
|
def pwa_install():
|
|
"""Instrukcja instalacji PWA na telefonie."""
|
|
return render_template('pwa_install.html')
|
|
|
|
|
|
@bp.route('/sw.js')
|
|
def service_worker():
|
|
"""Service worker served from root scope for PWA installability."""
|
|
return current_app.send_static_file('sw.js'), 200, {
|
|
'Content-Type': 'application/javascript',
|
|
'Service-Worker-Allowed': '/'
|
|
}
|
|
|
|
|
|
@bp.route('/robots.txt')
|
|
def robots_txt():
|
|
"""Robots.txt for search engine crawlers."""
|
|
content = """User-agent: *
|
|
Allow: /
|
|
Disallow: /admin/
|
|
Disallow: /api/
|
|
Disallow: /chat
|
|
Disallow: /login
|
|
Disallow: /register
|
|
Disallow: /dashboard
|
|
Sitemap: https://nordabiznes.pl/sitemap.xml
|
|
"""
|
|
return Response(content, mimetype='text/plain')
|
|
|
|
|
|
@bp.route('/sitemap.xml')
|
|
def sitemap_xml():
|
|
"""Dynamic sitemap XML with all public pages, companies, events, forum topics."""
|
|
from database import SessionLocal, Company, NordaEvent, ForumTopic
|
|
today = date.today().isoformat()
|
|
base = 'https://nordabiznes.pl'
|
|
|
|
urls = []
|
|
|
|
# Static pages
|
|
urls.append(('/', today, 'daily', '1.0'))
|
|
urls.append(('/szukaj', today, 'daily', '0.8'))
|
|
urls.append(('/release-notes', today, 'weekly', '0.5'))
|
|
urls.append(('/zainstaluj-aplikacje', today, 'monthly', '0.4'))
|
|
urls.append(('/polityka-prywatnosci', today, 'monthly', '0.3'))
|
|
urls.append(('/regulamin', today, 'monthly', '0.3'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Company pages — highest SEO value
|
|
companies = db.query(Company).filter(
|
|
Company.status == 'active'
|
|
).all()
|
|
for c in companies:
|
|
lastmod = today
|
|
urls.append((f'/company/{c.slug}', lastmod, 'weekly', '0.9'))
|
|
|
|
# Company list / catalog
|
|
urls.append(('/', today, 'daily', '1.0'))
|
|
|
|
# Calendar events (public, future + recent past)
|
|
events = db.query(NordaEvent).filter(
|
|
NordaEvent.event_date >= date.today() - timedelta(days=90)
|
|
).all()
|
|
for e in events:
|
|
lastmod = e.event_date.isoformat() if e.event_date else today
|
|
urls.append((f'/kalendarz/{e.id}', lastmod, 'weekly', '0.6'))
|
|
|
|
# Calendar index
|
|
urls.append(('/kalendarz/', today, 'weekly', '0.7'))
|
|
|
|
# Forum topics (public)
|
|
try:
|
|
topics = db.query(ForumTopic).filter(
|
|
ForumTopic.is_deleted == False # noqa: E712
|
|
).order_by(ForumTopic.created_at.desc()).limit(100).all()
|
|
for t in topics:
|
|
lastmod = t.last_reply_at.date().isoformat() if t.last_reply_at else (t.created_at.date().isoformat() if t.created_at else today)
|
|
urls.append((f'/forum/temat/{t.id}', lastmod, 'weekly', '0.6'))
|
|
urls.append(('/forum/', today, 'daily', '0.7'))
|
|
except Exception:
|
|
pass # Forum might not have topics
|
|
|
|
# B2B classifieds
|
|
urls.append(('/b2b', today, 'daily', '0.7'))
|
|
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
db.close()
|
|
|
|
# Build XML
|
|
xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
|
|
seen = set()
|
|
for loc, lastmod, freq, prio in urls:
|
|
if loc in seen:
|
|
continue
|
|
seen.add(loc)
|
|
xml_parts.append(f' <url><loc>{base}{loc}</loc><lastmod>{lastmod}</lastmod><changefreq>{freq}</changefreq><priority>{prio}</priority></url>')
|
|
xml_parts.append('</urlset>')
|
|
|
|
return Response('\n'.join(xml_parts), mimetype='application/xml')
|