nordabiz/blueprints/reports/routes.py
Maciej Pienczyn 66856a697d refactor(phase1): Extract blueprints for reports, contacts, classifieds, calendar
Phase 1 of app.py refactoring - reducing from ~14,455 to ~13,699 lines.

New structure:
- blueprints/reports/ - 4 routes (/raporty/*)
- blueprints/community/contacts/ - 6 routes (/kontakty/*)
- blueprints/community/classifieds/ - 4 routes (/tablica/*)
- blueprints/community/calendar/ - 3 routes (/kalendarz/*)
- utils/ - decorators, helpers, notifications, analytics
- extensions.py - Flask extensions (csrf, login_manager, limiter)
- config.py - environment configurations

Updated templates with blueprint-prefixed url_for() calls.

⚠️ DO NOT DEPLOY before presentation on 2026-01-30 19:00

Tested on DEV: all endpoints working correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 10:10:45 +01:00

186 lines
6.0 KiB
Python

"""
Reports Routes
==============
Business analytics and reporting endpoints.
"""
from datetime import datetime, date
from flask import render_template, url_for
from flask_login import login_required
from sqlalchemy import func
from sqlalchemy.orm import joinedload
from . import bp
from database import SessionLocal, Company, Category, CompanySocialMedia
@bp.route('/', endpoint='reports_index')
@login_required
def index():
"""Lista dostępnych raportów."""
reports = [
{
'id': 'staz-czlonkostwa',
'title': 'Staż członkostwa w Izbie NORDA',
'description': 'Zestawienie firm według daty przystąpienia do Izby. Pokazuje historię i lojalność członków.',
'icon': '🏆',
'url': url_for('.report_membership')
},
{
'id': 'social-media',
'title': 'Pokrycie Social Media',
'description': 'Analiza obecności firm w mediach społecznościowych: Facebook, Instagram, LinkedIn, YouTube, TikTok, X.',
'icon': '📱',
'url': url_for('.report_social_media')
},
{
'id': 'struktura-branzowa',
'title': 'Struktura branżowa',
'description': 'Rozkład firm według kategorii działalności: IT, Budownictwo, Usługi, Produkcja, Handel.',
'icon': '🏢',
'url': url_for('.report_categories')
},
]
return render_template('reports/index.html', reports=reports)
@bp.route('/staz-czlonkostwa', endpoint='report_membership')
@login_required
def membership():
"""Raport: Staż członkostwa w Izbie NORDA."""
db = SessionLocal()
try:
# Firmy z member_since, posortowane od najstarszego
companies = db.query(Company).filter(
Company.member_since.isnot(None)
).order_by(Company.member_since.asc()).all()
# Statystyki
today = date.today()
stats = {
'total_with_date': len(companies),
'total_without_date': db.query(Company).filter(
Company.member_since.is_(None)
).count(),
'oldest': companies[0] if companies else None,
'newest': companies[-1] if companies else None,
'avg_years': sum(
(today - c.member_since).days / 365.25
for c in companies
) / len(companies) if companies else 0
}
# Dodaj obliczony staż do każdej firmy
for c in companies:
c.membership_years = int((today - c.member_since).days / 365.25)
# Dodaj też do oldest i newest
if stats['oldest']:
stats['oldest'].membership_years = int((today - stats['oldest'].member_since).days / 365.25)
return render_template(
'reports/membership.html',
companies=companies,
stats=stats,
generated_at=datetime.now()
)
finally:
db.close()
@bp.route('/social-media', endpoint='report_social_media')
@login_required
def social_media():
"""Raport: Pokrycie Social Media."""
db = SessionLocal()
try:
# Wszystkie firmy z ich profilami social media
companies = db.query(Company).options(
joinedload(Company.social_media_profiles)
).order_by(Company.name).all()
platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'tiktok', 'twitter']
# Statystyki platform
platform_stats = {}
for platform in platforms:
count = db.query(CompanySocialMedia).filter_by(
platform=platform
).count()
platform_stats[platform] = {
'count': count,
'percent': round(count / len(companies) * 100, 1) if companies else 0
}
# Firmy z min. 1 profilem
companies_with_social = [
c for c in companies if c.social_media_profiles
]
stats = {
'total_companies': len(companies),
'with_social': len(companies_with_social),
'without_social': len(companies) - len(companies_with_social),
'coverage_percent': round(
len(companies_with_social) / len(companies) * 100, 1
) if companies else 0
}
return render_template(
'reports/social_media.html',
companies=companies,
platforms=platforms,
platform_stats=platform_stats,
stats=stats,
generated_at=datetime.now()
)
finally:
db.close()
@bp.route('/struktura-branzowa', endpoint='report_categories')
@login_required
def categories():
"""Raport: Struktura branżowa."""
db = SessionLocal()
try:
# Grupowanie po category_id (kolumna FK, nie relacja)
category_counts = db.query(
Company.category_id,
func.count(Company.id).label('count')
).group_by(Company.category_id).all()
total = sum(c.count for c in category_counts)
# Pobierz mapę kategorii (id -> name) jednym zapytaniem
category_map = {cat.id: cat.name for cat in db.query(Category).all()}
categories_list = []
for cat in category_counts:
cat_id = cat.category_id
cat_name = category_map.get(cat_id, 'Brak kategorii') if cat_id else 'Brak kategorii'
examples = db.query(Company.name).filter(
Company.category_id == cat_id
).limit(3).all()
categories_list.append({
'name': cat_name,
'count': cat.count,
'percent': round(cat.count / total * 100, 1) if total else 0,
'examples': [e.name for e in examples]
})
# Sortuj od największej
categories_list.sort(key=lambda x: x['count'], reverse=True)
return render_template(
'reports/categories.html',
categories=categories_list,
total=total,
generated_at=datetime.now()
)
finally:
db.close()