feat: Nowa sekcja Raporty w menu głównym
- Dodano link Raporty w menu nawigacyjnym (dla zalogowanych) - Utworzono 3 raporty generowane w czasie rzeczywistym: - Staż członkostwa w Izbie NORDA - Pokrycie Social Media (6 platform) - Struktura branżowa (kategorie firm) - Dodano dokumentację strategii monetyzacji 3-tier pricing - Release notes v1.18.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3c1e05baf5
commit
1b6e698d87
84
CLAUDE.md
84
CLAUDE.md
@ -962,3 +962,87 @@ membership_fees (
|
||||
- Dostęp do funduszy na rozwój platformy
|
||||
- Możliwość świadczenia płatnych usług
|
||||
- Elastyczność finansowa
|
||||
|
||||
## Strategia monetyzacji
|
||||
|
||||
### Model 3-tier Pricing (Kotwiczenie ceny)
|
||||
|
||||
**Strategia:** Trzy poziomy cenowe z zastosowaniem psychologii kotwiczenia ceny (Price Anchoring).
|
||||
Najwyższy poziom służy jako **kotwica** - sprawia że środkowy wydaje się atrakcyjny i jest docelowy.
|
||||
|
||||
| Poziom | Nazwa | Cena/mies. | Cel strategiczny |
|
||||
|--------|-------|------------|------------------|
|
||||
| **1** | Basic | ~49 zł | Entry point, ograniczone funkcje |
|
||||
| **2** | Premium | ~99 zł | **DOCELOWY** - rekomendowany, najlepsza wartość |
|
||||
| **3** | Enterprise | ~199 zł | **KOTWICA** - premium, pełny dostęp |
|
||||
|
||||
**Wyjątek:** Członkowie Izby NORDA płacący składki (~200 zł/mies.) mają specjalny status - dostęp Premium za symboliczne 1 zł.
|
||||
|
||||
### Psychologia 3-tier Pricing
|
||||
|
||||
- Ludzie naturalnie wybierają środkową opcję (efekt kompromisu)
|
||||
- Wysoka cena kotwicy sprawia że środkowa wydaje się "okazją"
|
||||
- Niska cena Basic sprawia że użytkownik czuje "upgrade jest wart dopłaty"
|
||||
- Firmy stosujące 3-tier widzą ~30% wzrost przychodów
|
||||
- Slack: dodanie Enterprise tier zwiększyło konwersję na Professional o 28%
|
||||
|
||||
### Matryca dostępu do funkcji
|
||||
|
||||
| Funkcja | Basic | Premium | Enterprise |
|
||||
|---------|:-----:|:-------:|:----------:|
|
||||
| Katalog firm | ✅ | ✅ | ✅ |
|
||||
| Profil firmy | ✅ | ✅ | ✅ |
|
||||
| Forum | ❌ | ✅ | ✅ |
|
||||
| Kalendarz wydarzeń | ❌ | ✅ | ✅ |
|
||||
| Chat AI (NordaGPT) | ❌ | ✅ | ✅ |
|
||||
| **Raporty podstawowe** | ❌ | ✅ | ✅ |
|
||||
| **Raporty zaawansowane** | ❌ | ❌ | ✅ |
|
||||
| Eksport danych (CSV/PDF) | ❌ | ❌ | ✅ |
|
||||
| API dostęp | ❌ | ❌ | ✅ |
|
||||
| Priorytetowe wsparcie | ❌ | ❌ | ✅ |
|
||||
|
||||
### Implementacja techniczna (przyszłość)
|
||||
|
||||
```python
|
||||
# Model User - nowe pola
|
||||
class User(Base):
|
||||
# ...
|
||||
subscription_tier = Column(String(20), default='basic') # basic, premium, enterprise
|
||||
subscription_expires_at = Column(DateTime)
|
||||
is_norda_member = Column(Boolean, default=False) # Członek Izby = specjalny status
|
||||
|
||||
# Dekorator kontroli dostępu
|
||||
def requires_tier(min_tier):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
tiers = ['basic', 'premium', 'enterprise']
|
||||
user_tier_idx = tiers.index(current_user.subscription_tier)
|
||||
required_idx = tiers.index(min_tier)
|
||||
if user_tier_idx < required_idx:
|
||||
flash(f'Ta funkcja wymaga konta {min_tier.title()}.', 'warning')
|
||||
return redirect(url_for('pricing'))
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
# Użycie
|
||||
@app.route('/raporty/zaawansowane')
|
||||
@login_required
|
||||
@requires_tier('enterprise')
|
||||
def advanced_reports():
|
||||
...
|
||||
```
|
||||
|
||||
### Raporty - podział według poziomu
|
||||
|
||||
**Raporty podstawowe (Premium+):**
|
||||
- Staż członkostwa w Izbie NORDA
|
||||
- Pokrycie Social Media
|
||||
- Struktura branżowa
|
||||
|
||||
**Raporty zaawansowane (Enterprise only):**
|
||||
- Ranking SEO
|
||||
- Mapa lokalizacji
|
||||
- Sieć rekomendacji
|
||||
- Aktywność w wydarzeniach
|
||||
|
||||
184
app.py
184
app.py
@ -8541,6 +8541,178 @@ def api_it_audit_export():
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# RAPORTY
|
||||
# ============================================================
|
||||
|
||||
@app.route('/raporty')
|
||||
@login_required
|
||||
def reports_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)
|
||||
|
||||
|
||||
@app.route('/raporty/staz-czlonkostwa')
|
||||
@login_required
|
||||
def report_membership():
|
||||
"""Raport: Staż członkostwa w Izbie NORDA."""
|
||||
from datetime import date
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/raporty/social-media')
|
||||
@login_required
|
||||
def report_social_media():
|
||||
"""Raport: Pokrycie Social Media."""
|
||||
from sqlalchemy.orm import joinedload
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/raporty/struktura-branzowa')
|
||||
@login_required
|
||||
def report_categories():
|
||||
"""Raport: Struktura branżowa."""
|
||||
from sqlalchemy import func
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Grupowanie po kategoriach
|
||||
category_counts = db.query(
|
||||
Company.category,
|
||||
func.count(Company.id).label('count')
|
||||
).group_by(Company.category).all()
|
||||
|
||||
total = sum(c.count for c in category_counts)
|
||||
|
||||
categories = []
|
||||
for cat in category_counts:
|
||||
cat_name = cat.category or 'Brak kategorii'
|
||||
examples = db.query(Company.name).filter_by(
|
||||
category=cat.category
|
||||
).limit(3).all()
|
||||
|
||||
categories.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.sort(key=lambda x: x['count'], reverse=True)
|
||||
|
||||
return render_template(
|
||||
'reports/categories.html',
|
||||
categories=categories,
|
||||
total=total,
|
||||
generated_at=datetime.now()
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# RELEASE NOTES
|
||||
# ============================================================
|
||||
@ -8549,6 +8721,18 @@ def api_it_audit_export():
|
||||
def release_notes():
|
||||
"""Historia zmian platformy."""
|
||||
releases = [
|
||||
{
|
||||
'version': 'v1.18.0',
|
||||
'date': '14 stycznia 2026',
|
||||
'badges': ['new'],
|
||||
'new': [
|
||||
'Nowa sekcja: Raporty - dostępna z menu głównego dla zalogowanych',
|
||||
'Raport: Staż członkostwa w Izbie NORDA (sortowanie od najstarszego członka)',
|
||||
'Raport: Pokrycie Social Media (analiza 6 platform dla wszystkich firm)',
|
||||
'Raport: Struktura branżowa (rozkład firm wg kategorii działalności)',
|
||||
'Raporty generowane w czasie rzeczywistym z informacją o źródle danych',
|
||||
],
|
||||
},
|
||||
{
|
||||
'version': 'v1.17.0',
|
||||
'date': '14 stycznia 2026',
|
||||
|
||||
@ -932,6 +932,9 @@
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Raporty -->
|
||||
<li><a href="{{ url_for('reports_index') }}" class="nav-link {% if request.endpoint and request.endpoint.startswith('report') %}active{% endif %}">Raporty</a></li>
|
||||
|
||||
<!-- Notifications -->
|
||||
<li class="notifications-dropdown">
|
||||
<button class="notifications-trigger nav-link-with-badge" onclick="toggleNotifications(event)" aria-label="Powiadomienia">
|
||||
|
||||
457
templates/reports/categories.html
Normal file
457
templates/reports/categories.html
Normal file
@ -0,0 +1,457 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Struktura branżowa - Raporty - Norda Biznes Hub{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.report-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-header .back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header .back-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.report-header h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header h1 span {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border: 1px solid #86efac;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.report-meta-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.highlight {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.category-card.rank-1::before {
|
||||
background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-2::before {
|
||||
background: linear-gradient(180deg, #a855f7 0%, #9333ea 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-3::before {
|
||||
background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.category-count-badge {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-card.rank-1 .category-count-badge {
|
||||
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-2 .category-count-badge {
|
||||
background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-3 .category-count-badge {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.category-bar {
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.category-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: var(--radius);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.category-card.rank-1 .category-bar-fill {
|
||||
background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-2 .category-bar-fill {
|
||||
background: linear-gradient(90deg, #a855f7 0%, #9333ea 100%);
|
||||
}
|
||||
|
||||
.category-card.rank-3 .category-bar-fill {
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.category-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.category-examples {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.category-examples strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.data-table-container {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.data-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.rank-badge.gold {
|
||||
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rank-badge.silver {
|
||||
background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rank-badge.bronze {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rank-badge.regular {
|
||||
background: var(--background);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.percent-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.percent-bar-bg {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.percent-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.percent-value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.categories-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="report-header">
|
||||
<a href="{{ url_for('reports_index') }}" class="back-link">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Powrót do raportów
|
||||
</a>
|
||||
<h1><span>🏢</span> Struktura branżowa</h1>
|
||||
</div>
|
||||
|
||||
<div class="report-meta">
|
||||
<div class="report-meta-grid">
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }}</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
||||
</svg>
|
||||
<span>Źródło: baza danych nordabiznes.pl</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
<span>Firm w raporcie: {{ total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon">🏆</div>
|
||||
<div class="stat-value">{{ categories[0].name if categories else '-' }}</div>
|
||||
<div class="stat-label">Największa kategoria</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-value">{{ categories|length }}</div>
|
||||
<div class="stat-label">Kategorii łącznie</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🏢</div>
|
||||
<div class="stat-value">{{ total }}</div>
|
||||
<div class="stat-label">Firm łącznie</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">📊 Przegląd kategorii</h2>
|
||||
<div class="categories-grid">
|
||||
{% for category in categories[:6] %}
|
||||
<div class="category-card {% if loop.index == 1 %}rank-1{% elif loop.index == 2 %}rank-2{% elif loop.index == 3 %}rank-3{% endif %}">
|
||||
<div class="category-header">
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
<div class="category-count-badge">{{ category.count }} firm</div>
|
||||
</div>
|
||||
<div class="category-bar">
|
||||
<div class="category-bar-fill" style="width: {{ category.percent }}%"></div>
|
||||
</div>
|
||||
<div class="category-stats">
|
||||
<div class="category-stat">
|
||||
<span>📈</span>
|
||||
<span>{{ category.percent }}% udziału</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-examples">
|
||||
<strong>Przykłady:</strong> {{ category.examples|join(', ') }}{% if category.count > 3 %}...{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">📋 Pełna tabela</h2>
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">#</th>
|
||||
<th>Kategoria</th>
|
||||
<th style="width: 100px;">Liczba firm</th>
|
||||
<th>Udział</th>
|
||||
<th>Przykłady firm</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if loop.index == 1 %}
|
||||
<span class="rank-badge gold">1</span>
|
||||
{% elif loop.index == 2 %}
|
||||
<span class="rank-badge silver">2</span>
|
||||
{% elif loop.index == 3 %}
|
||||
<span class="rank-badge bronze">3</span>
|
||||
{% else %}
|
||||
<span class="rank-badge regular">{{ loop.index }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><strong>{{ category.name }}</strong></td>
|
||||
<td>{{ category.count }}</td>
|
||||
<td>
|
||||
<div class="percent-bar">
|
||||
<div class="percent-bar-bg">
|
||||
<div class="percent-bar-fill" style="width: {{ category.percent }}%"></div>
|
||||
</div>
|
||||
<span class="percent-value">{{ category.percent }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ category.examples|join(', ') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
174
templates/reports/index.html
Normal file
174
templates/reports/index.html
Normal file
@ -0,0 +1,174 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Raporty - Norda Biznes Hub{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.reports-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.reports-header h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.reports-header p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.reports-info-banner {
|
||||
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
|
||||
border: 1px solid #7dd3fc;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.reports-info-banner svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #0284c7;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.reports-info-banner .info-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reports-info-banner .info-title {
|
||||
font-weight: 600;
|
||||
color: #0c4a6e;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.reports-info-banner .info-text {
|
||||
color: #0369a1;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.reports-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.report-card {
|
||||
background: var(--surface);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.report-icon {
|
||||
font-size: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.report-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.report-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.report-link:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.report-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.report-card:hover .report-link svg {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.report-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-link {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="reports-header">
|
||||
<h1>Raporty</h1>
|
||||
<p>Analizy i statystyki członków Izby NORDA</p>
|
||||
</div>
|
||||
|
||||
<div class="reports-info-banner">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="info-content">
|
||||
<div class="info-title">Raporty generowane w czasie rzeczywistym</div>
|
||||
<div class="info-text">
|
||||
Wszystkie raporty są generowane na bieżąco z danych systemu nordabiznes.pl.
|
||||
Każdy raport zawiera informację o czasie wygenerowania i źródle danych.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reports-grid">
|
||||
{% for report in reports %}
|
||||
<div class="report-card">
|
||||
<div class="report-icon">{{ report.icon }}</div>
|
||||
<div class="report-content">
|
||||
<div class="report-title">{{ report.title }}</div>
|
||||
<div class="report-description">{{ report.description }}</div>
|
||||
<a href="{{ report.url }}" class="report-link">
|
||||
Zobacz raport
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
324
templates/reports/membership.html
Normal file
324
templates/reports/membership.html
Normal file
@ -0,0 +1,324 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Staż członkostwa - Raporty - Norda Biznes Hub{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.report-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-header .back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header .back-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.report-header h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header h1 span {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border: 1px solid #86efac;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.report-meta-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.highlight {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted, #9ca3af);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.data-table-container {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.data-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.company-link {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.company-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.years-badge {
|
||||
display: inline-block;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.years-badge.veteran {
|
||||
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
.years-badge.senior {
|
||||
background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%);
|
||||
}
|
||||
|
||||
.years-badge.regular {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.years-badge.new {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
display: inline-block;
|
||||
background: var(--background);
|
||||
color: var(--text-secondary);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.row-number {
|
||||
color: var(--text-muted, #9ca3af);
|
||||
font-size: var(--font-size-sm);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.data-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="report-header">
|
||||
<a href="{{ url_for('reports_index') }}" class="back-link">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Powrót do raportów
|
||||
</a>
|
||||
<h1><span>🏆</span> Staż członkostwa w Izbie NORDA</h1>
|
||||
</div>
|
||||
|
||||
<div class="report-meta">
|
||||
<div class="report-meta-grid">
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }}</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
||||
</svg>
|
||||
<span>Źródło: baza danych nordabiznes.pl</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
<span>Firm w raporcie: {{ stats.total_with_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon">👑</div>
|
||||
<div class="stat-value">{{ stats.oldest.name if stats.oldest else '-' }}</div>
|
||||
<div class="stat-label">Najstarszy członek</div>
|
||||
<div class="stat-detail">
|
||||
{% if stats.oldest %}
|
||||
od {{ stats.oldest.member_since.strftime('%d.%m.%Y') }} ({{ stats.oldest.membership_years }} lat)
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🆕</div>
|
||||
<div class="stat-value">{{ stats.newest.name if stats.newest else '-' }}</div>
|
||||
<div class="stat-label">Najnowszy członek</div>
|
||||
<div class="stat-detail">
|
||||
{% if stats.newest %}
|
||||
od {{ stats.newest.member_since.strftime('%d.%m.%Y') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-value">{{ "%.1f"|format(stats.avg_years) }} lat</div>
|
||||
<div class="stat-label">Średni staż</div>
|
||||
<div class="stat-detail">wszystkich członków</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">❓</div>
|
||||
<div class="stat-value">{{ stats.total_without_date }}</div>
|
||||
<div class="stat-label">Bez daty</div>
|
||||
<div class="stat-detail">firmy do uzupełnienia</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Firma</th>
|
||||
<th>Data przystąpienia</th>
|
||||
<th>Staż</th>
|
||||
<th>Kategoria</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for company in companies %}
|
||||
<tr>
|
||||
<td class="row-number">{{ loop.index }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('company_detail', slug=company.slug) }}" class="company-link">
|
||||
{{ company.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ company.member_since.strftime('%d.%m.%Y') }}</td>
|
||||
<td>
|
||||
{% if company.membership_years >= 20 %}
|
||||
<span class="years-badge veteran">{{ company.membership_years }} lat</span>
|
||||
{% elif company.membership_years >= 10 %}
|
||||
<span class="years-badge senior">{{ company.membership_years }} lat</span>
|
||||
{% elif company.membership_years >= 5 %}
|
||||
<span class="years-badge regular">{{ company.membership_years }} lat</span>
|
||||
{% else %}
|
||||
<span class="years-badge new">{{ company.membership_years }} lat</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if company.category %}
|
||||
<span class="category-badge">{{ company.category }}</span>
|
||||
{% else %}
|
||||
<span class="category-badge">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
389
templates/reports/social_media.html
Normal file
389
templates/reports/social_media.html
Normal file
@ -0,0 +1,389 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Pokrycie Social Media - Raporty - Norda Biznes Hub{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.report-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-header .back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header .back-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.report-header h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-header h1 span {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border: 1px solid #86efac;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.report-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.report-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.report-meta-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card.highlight {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.platforms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.platform-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.platform-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.platform-count {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.platform-percent {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table-container {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.data-table th.platform-col {
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.data-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.company-link {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.company-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.check-icon.has {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.check-icon.missing {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.platform-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
display: inline-block;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.total-badge.high {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.total-badge.medium {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.total-badge.low {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
}
|
||||
|
||||
.total-badge.none {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.data-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
min-width: 700px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="report-header">
|
||||
<a href="{{ url_for('reports_index') }}" class="back-link">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Powrót do raportów
|
||||
</a>
|
||||
<h1><span>📱</span> Pokrycie Social Media</h1>
|
||||
</div>
|
||||
|
||||
<div class="report-meta">
|
||||
<div class="report-meta-grid">
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }}</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
||||
</svg>
|
||||
<span>Źródło: baza danych nordabiznes.pl</span>
|
||||
</div>
|
||||
<div class="report-meta-item">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
<span>Firm w raporcie: {{ stats.total_companies }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-value">{{ stats.with_social }}</div>
|
||||
<div class="stat-label">Firmy z Social Media</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">❌</div>
|
||||
<div class="stat-value">{{ stats.without_social }}</div>
|
||||
<div class="stat-label">Bez Social Media</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📈</div>
|
||||
<div class="stat-value">{{ stats.coverage_percent }}%</div>
|
||||
<div class="stat-label">Pokrycie</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">📊 Statystyki platform</h2>
|
||||
<div class="platforms-grid">
|
||||
{% for platform in platforms %}
|
||||
<div class="platform-card">
|
||||
<div class="platform-icon">
|
||||
{% if platform == 'facebook' %}📘
|
||||
{% elif platform == 'instagram' %}📷
|
||||
{% elif platform == 'linkedin' %}💼
|
||||
{% elif platform == 'youtube' %}📺
|
||||
{% elif platform == 'tiktok' %}🎵
|
||||
{% elif platform == 'twitter' %}🐦
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="platform-name">{{ platform }}</div>
|
||||
<div class="platform-count">{{ platform_stats[platform].count }}</div>
|
||||
<div class="platform-percent">{{ platform_stats[platform].percent }}%</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">📋 Szczegółowa tabela</h2>
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Firma</th>
|
||||
<th class="platform-col" title="Facebook">FB</th>
|
||||
<th class="platform-col" title="Instagram">IG</th>
|
||||
<th class="platform-col" title="LinkedIn">LI</th>
|
||||
<th class="platform-col" title="YouTube">YT</th>
|
||||
<th class="platform-col" title="TikTok">TT</th>
|
||||
<th class="platform-col" title="Twitter/X">X</th>
|
||||
<th>Razem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for company in companies %}
|
||||
{% set profile_count = company.social_media_profiles|length %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('company_detail', slug=company.slug) }}" class="company-link">
|
||||
{{ company.name }}
|
||||
</a>
|
||||
</td>
|
||||
{% for platform in platforms %}
|
||||
<td class="platform-cell">
|
||||
{% set has_platform = company.social_media_profiles|selectattr('platform', 'equalto', platform)|list|length > 0 %}
|
||||
{% if has_platform %}
|
||||
<span class="check-icon has">✓</span>
|
||||
{% else %}
|
||||
<span class="check-icon missing">✗</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
{% if profile_count >= 4 %}
|
||||
<span class="total-badge high">{{ profile_count }}</span>
|
||||
{% elif profile_count >= 2 %}
|
||||
<span class="total-badge medium">{{ profile_count }}</span>
|
||||
{% elif profile_count == 1 %}
|
||||
<span class="total-badge low">{{ profile_count }}</span>
|
||||
{% else %}
|
||||
<span class="total-badge none">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user