feat: enrich GBP audit detail page with comprehensive data display
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
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
Add opening hours, business status, Google types, website URL, Place ID, rating/reviews/photos summary, website tracking indicators, Google attributes, Places API reviews, smart recommendations engine, and benchmarks comparison with other Norda Biznes members. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9bb9085810
commit
cbb48f2726
@ -400,8 +400,69 @@ def gbp_audit_dashboard(slug):
|
||||
'google_review_response_rate': float(analysis.google_review_response_rate) if getattr(analysis, 'google_review_response_rate', None) is not None else None,
|
||||
'google_posts_data': getattr(analysis, 'google_posts_data', None),
|
||||
'google_posts_count': getattr(analysis, 'google_posts_count', None),
|
||||
# Additional data not previously exposed
|
||||
'google_business_status': analysis.google_business_status,
|
||||
'google_opening_hours': analysis.google_opening_hours,
|
||||
'google_photos_count': analysis.google_photos_count,
|
||||
'google_website': analysis.google_website,
|
||||
'google_types': analysis.google_types,
|
||||
'google_maps_url': analysis.google_maps_url,
|
||||
'google_rating': float(analysis.google_rating) if analysis.google_rating is not None else None,
|
||||
'google_reviews_count': analysis.google_reviews_count,
|
||||
'google_attributes': getattr(analysis, 'google_attributes', None),
|
||||
'google_reviews_data': getattr(analysis, 'google_reviews_data', None),
|
||||
'google_photos_metadata': getattr(analysis, 'google_photos_metadata', None),
|
||||
'google_place_id': analysis.google_place_id,
|
||||
'has_google_analytics': getattr(analysis, 'has_google_analytics', None),
|
||||
'has_google_tag_manager': getattr(analysis, 'has_google_tag_manager', None),
|
||||
'has_google_maps_embed': getattr(analysis, 'has_google_maps_embed', None),
|
||||
}
|
||||
|
||||
# Build GBP recommendations
|
||||
gbp_recommendations = []
|
||||
if analysis.google_name:
|
||||
if not analysis.google_opening_hours:
|
||||
gbp_recommendations.append({'severity': 'warning', 'text': 'Brak godzin otwarcia w Google. Klienci nie wiedza kiedy firma jest czynna.'})
|
||||
if not analysis.google_phone:
|
||||
gbp_recommendations.append({'severity': 'critical', 'text': 'Brak numeru telefonu w Google. To podstawowy sposob kontaktu klientow.'})
|
||||
if (analysis.google_photos_count or 0) < 5:
|
||||
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 zdjec w profilu Google. Dodaj zdjecia — firmy ze zdjeciami otrzymuja 42%% wiecej zapytan o dojazd.'})
|
||||
if (analysis.google_reviews_count or 0) < 5:
|
||||
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 opinii Google. Zachecaj klientow do wystawienia recenzji.'})
|
||||
rr = float(analysis.google_review_response_rate) if analysis.google_review_response_rate is not None else None
|
||||
if rr is not None and rr < 50:
|
||||
gbp_recommendations.append({'severity': 'warning', 'text': 'Odpowiedzi na mniej niz 50%% opinii. Odpowiadanie na recenzje poprawia widocznosc.'})
|
||||
if not analysis.google_website:
|
||||
gbp_recommendations.append({'severity': 'info', 'text': 'Brak strony WWW w profilu Google. Dodaj link do strony firmy.'})
|
||||
if analysis.google_business_status and analysis.google_business_status != 'OPERATIONAL':
|
||||
gbp_recommendations.append({'severity': 'critical', 'text': 'Profil Google nie ma statusu OPERATIONAL. Sprawdz czy firma jest oznaczona jako czynna.'})
|
||||
if not gbp_recommendations:
|
||||
gbp_recommendations.append({'severity': 'success', 'text': 'Profil Google wyglada dobrze! Nie znaleziono powaznych problemow.'})
|
||||
|
||||
# GBP benchmarks — average across all members
|
||||
gbp_benchmarks = {}
|
||||
try:
|
||||
from sqlalchemy import func as sqlfunc
|
||||
bench = db.query(
|
||||
sqlfunc.avg(CompanyWebsiteAnalysis.google_rating),
|
||||
sqlfunc.avg(CompanyWebsiteAnalysis.google_reviews_count),
|
||||
sqlfunc.avg(CompanyWebsiteAnalysis.google_photos_count),
|
||||
sqlfunc.count(CompanyWebsiteAnalysis.id),
|
||||
).join(Company, Company.id == CompanyWebsiteAnalysis.company_id
|
||||
).filter(
|
||||
Company.status == 'active',
|
||||
CompanyWebsiteAnalysis.google_rating.isnot(None),
|
||||
).first()
|
||||
if bench and bench[3] > 1:
|
||||
gbp_benchmarks = {
|
||||
'count': bench[3],
|
||||
'avg_rating': round(float(bench[0] or 0), 1),
|
||||
'avg_reviews': round(float(bench[1] or 0)),
|
||||
'avg_photos': round(float(bench[2] or 0)),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If no audit exists, we still render the page (template handles this)
|
||||
# The user can trigger an audit from the dashboard
|
||||
|
||||
@ -417,7 +478,9 @@ def gbp_audit_dashboard(slug):
|
||||
places_data=places_data,
|
||||
can_audit=can_audit,
|
||||
gbp_audit_available=GBP_AUDIT_AVAILABLE,
|
||||
gbp_audit_version=GBP_AUDIT_VERSION
|
||||
gbp_audit_version=GBP_AUDIT_VERSION,
|
||||
gbp_recommendations=gbp_recommendations if analysis else [],
|
||||
gbp_benchmarks=gbp_benchmarks if analysis else {}
|
||||
)
|
||||
|
||||
finally:
|
||||
|
||||
@ -1021,7 +1021,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if places_data and (places_data.primary_type or places_data.editorial_summary or places_data.price_level) %}
|
||||
{% if places_data and (places_data.primary_type or places_data.editorial_summary or places_data.price_level or places_data.google_business_status or places_data.google_website or places_data.google_types) %}
|
||||
<!-- Google Places Enrichment Data -->
|
||||
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
|
||||
@ -1030,20 +1030,63 @@
|
||||
</svg>
|
||||
Dane z Google Places
|
||||
</h3>
|
||||
|
||||
{# Business status badge #}
|
||||
{% if places_data.google_business_status %}
|
||||
<div style="margin-bottom: var(--spacing-sm);">
|
||||
{% set bs = places_data.google_business_status %}
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
|
||||
{% if bs == 'OPERATIONAL' %}background: #d1fae5; color: #065f46;
|
||||
{% elif bs == 'CLOSED_TEMPORARILY' %}background: #fef3c7; color: #92400e;
|
||||
{% else %}background: #fee2e2; color: #991b1b;{% endif %}">
|
||||
<span style="width: 8px; height: 8px; border-radius: 50%;
|
||||
{% if bs == 'OPERATIONAL' %}background: #10b981;
|
||||
{% elif bs == 'CLOSED_TEMPORARILY' %}background: #f59e0b;
|
||||
{% else %}background: #ef4444;{% endif %}"></span>
|
||||
{% if bs == 'OPERATIONAL' %}Firma czynna
|
||||
{% elif bs == 'CLOSED_TEMPORARILY' %}Tymczasowo zamknięta
|
||||
{% elif bs == 'CLOSED_PERMANENTLY' %}Zamknięta na stałe
|
||||
{% else %}{{ bs|replace('_', ' ')|title }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if places_data.primary_type %}
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Typ:</span>
|
||||
<span style="color: var(--text-primary); font-weight: 500;">{{ places_data.primary_type|replace('_', ' ')|title }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.google_types and places_data.google_types is iterable and places_data.google_types is not string %}
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Wszystkie kategorie:</span>
|
||||
<span style="color: var(--text-secondary);">
|
||||
{% for t in places_data.google_types[:8] %}
|
||||
<span style="display: inline-block; padding: 1px 6px; margin: 1px; background: #eff6ff; color: #1d4ed8; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">{{ t|replace('_', ' ')|title }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.editorial_summary %}
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Opis Google:</span>
|
||||
<span style="color: var(--text-secondary);">{{ places_data.editorial_summary }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.google_website %}
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Strona WWW:</span>
|
||||
<a href="{{ places_data.google_website }}" target="_blank" rel="noopener" style="color: #2563eb; text-decoration: none;">{{ places_data.google_website[:60] }}{% if places_data.google_website|length > 60 %}...{% endif %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.google_place_id %}
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Place ID:</span>
|
||||
<span style="color: var(--text-secondary); font-family: monospace; font-size: 11px;">{{ places_data.google_place_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.price_level is not none and places_data.price_level %}
|
||||
<div style="font-size: var(--font-size-sm);">
|
||||
<div style="margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm);">
|
||||
<span style="color: var(--text-tertiary);">Poziom cen:</span>
|
||||
<span style="color: var(--text-primary);">
|
||||
{% if places_data.price_level == 'PRICE_LEVEL_FREE' %}Bezpłatne
|
||||
@ -1056,6 +1099,131 @@
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Google rating and reviews summary #}
|
||||
{% if places_data.google_rating is not none %}
|
||||
<div style="margin-top: var(--spacing-sm); padding-top: var(--spacing-sm); border-top: 1px solid var(--border); display: flex; gap: var(--spacing-lg); flex-wrap: wrap; font-size: var(--font-size-sm);">
|
||||
<div>
|
||||
<span style="color: var(--text-tertiary);">Ocena:</span>
|
||||
<span style="font-weight: 700; color: #f59e0b;">
|
||||
{% for i in range(5) %}
|
||||
<span style="color: {{ '#f59e0b' if i < (places_data.google_rating|round) else '#d1d5db' }};">★</span>
|
||||
{% endfor %}
|
||||
{{ places_data.google_rating }}/5
|
||||
</span>
|
||||
</div>
|
||||
{% if places_data.google_reviews_count %}
|
||||
<div>
|
||||
<span style="color: var(--text-tertiary);">Opinii:</span>
|
||||
<span style="font-weight: 600; color: var(--text-primary);">{{ places_data.google_reviews_count }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.google_photos_count %}
|
||||
<div>
|
||||
<span style="color: var(--text-tertiary);">Zdjęć:</span>
|
||||
<span style="font-weight: 600; color: var(--text-primary);">{{ places_data.google_photos_count }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Opening Hours Section #}
|
||||
{% if places_data and places_data.google_opening_hours %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
|
||||
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Godziny otwarcia w Google
|
||||
</h3>
|
||||
{% set hours = places_data.google_opening_hours %}
|
||||
{% if hours is mapping and hours.get('weekday_text') %}
|
||||
{# Standard Google Places API format #}
|
||||
{% set day_names = {'Monday': 'Poniedziałek', 'Tuesday': 'Wtorek', 'Wednesday': 'Środa', 'Thursday': 'Czwartek', 'Friday': 'Piątek', 'Saturday': 'Sobota', 'Sunday': 'Niedziela'} %}
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px var(--spacing-md); font-size: var(--font-size-sm);">
|
||||
{% for day_line in hours.weekday_text %}
|
||||
{% set parts = day_line.split(': ', 1) %}
|
||||
<span style="color: var(--text-tertiary); font-weight: 500;">{{ day_names.get(parts[0], parts[0]) if parts|length > 0 else day_line }}:</span>
|
||||
<span style="color: var(--text-primary);">{{ parts[1] if parts|length > 1 else '' }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif hours is mapping and hours.get('periods') %}
|
||||
{# Alternative format with periods #}
|
||||
{% set day_pl = ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'] %}
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px var(--spacing-md); font-size: var(--font-size-sm);">
|
||||
{% for period in hours.periods %}
|
||||
<span style="color: var(--text-tertiary); font-weight: 500;">{{ day_pl[period.open.day|int] if period.open and period.open.day is defined else '?' }}:</span>
|
||||
<span style="color: var(--text-primary);">
|
||||
{% if period.open and period.open.time is defined %}
|
||||
{{ period.open.time[:2] }}:{{ period.open.time[2:] }}
|
||||
{% if period.close and period.close.time is defined %} – {{ period.close.time[:2] }}:{{ period.close.time[2:] }}{% endif %}
|
||||
{% else %}Zamknięte{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif hours is iterable and hours is not string and hours is not mapping %}
|
||||
{# List format #}
|
||||
<div style="font-size: var(--font-size-sm);">
|
||||
{% for item in hours %}
|
||||
<div style="color: var(--text-primary);">{{ item }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">Godziny ustawione (format niestandardowy)</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Website Tracking Indicators #}
|
||||
{% if places_data and (places_data.has_google_analytics is not none or places_data.has_google_tag_manager is not none or places_data.has_google_maps_embed is not none) %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
|
||||
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
Integracja ze stroną WWW
|
||||
</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
|
||||
{% if places_data.has_google_analytics is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_analytics else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if places_data.has_google_analytics else '#ef4444' }};">{{ '✓' if places_data.has_google_analytics else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Google Analytics</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.has_google_tag_manager is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_tag_manager else '#fee2e2' }};">
|
||||
<span style="color: {{ '#10b981' if places_data.has_google_tag_manager else '#ef4444' }};">{{ '✓' if places_data.has_google_tag_manager else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Google Tag Manager</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if places_data.has_google_maps_embed is not none %}
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if places_data.has_google_maps_embed else '#f3f4f6' }};">
|
||||
<span style="color: {{ '#10b981' if places_data.has_google_maps_embed else '#6b7280' }};">{{ '✓' if places_data.has_google_maps_embed else '✗' }}</span>
|
||||
<span style="font-size: var(--font-size-sm);">Mapa Google na stronie</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Google Attributes #}
|
||||
{% if places_data and places_data.google_attributes and places_data.google_attributes is mapping %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-md); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h3 style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; display: flex; align-items: center; gap: var(--spacing-xs);">
|
||||
<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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
||||
</svg>
|
||||
Atrybuty Google Business
|
||||
</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-xs);">
|
||||
{% for key, value in places_data.google_attributes.items() %}
|
||||
<span style="padding: 4px 10px; background: {{ '#dcfce7' if value else '#f3f4f6' }}; color: {{ '#10b981' if value else '#6b7280' }}; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">
|
||||
{{ key|replace('_', ' ')|replace('.', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} ✗{% endif %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -1741,6 +1909,146 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Smart Recommendations (from backend analysis) #}
|
||||
{% if gbp_recommendations and gbp_recommendations|length > 0 %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h2 class="section-title">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
Automatyczne zalecenia
|
||||
</h2>
|
||||
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
|
||||
{% for rec in gbp_recommendations %}
|
||||
<div style="padding: var(--spacing-md); border-radius: var(--radius); display: flex; align-items: flex-start; gap: var(--spacing-sm);
|
||||
{% if rec.severity == 'critical' %}background: #fef2f2; border-left: 4px solid #ef4444;
|
||||
{% elif rec.severity == 'warning' %}background: #fffbeb; border-left: 4px solid #f59e0b;
|
||||
{% elif rec.severity == 'success' %}background: #f0fdf4; border-left: 4px solid #10b981;
|
||||
{% else %}background: #eff6ff; border-left: 4px solid #3b82f6;{% endif %}">
|
||||
<span style="flex-shrink: 0; font-size: 18px;">
|
||||
{% if rec.severity == 'critical' %}⚠
|
||||
{% elif rec.severity == 'warning' %}⚠
|
||||
{% elif rec.severity == 'success' %}✔
|
||||
{% else %}ℹ{% endif %}
|
||||
</span>
|
||||
<div>
|
||||
<span style="font-size: var(--font-size-xs); font-weight: 600; text-transform: uppercase; padding: 1px 6px; border-radius: var(--radius-sm); margin-bottom: 4px; display: inline-block;
|
||||
{% if rec.severity == 'critical' %}background: #fee2e2; color: #991b1b;
|
||||
{% elif rec.severity == 'warning' %}background: #fef3c7; color: #92400e;
|
||||
{% elif rec.severity == 'success' %}background: #dcfce7; color: #166534;
|
||||
{% else %}background: #dbeafe; color: #1e40af;{% endif %}">
|
||||
{% if rec.severity == 'critical' %}Krytyczne
|
||||
{% elif rec.severity == 'warning' %}Zalecenie
|
||||
{% elif rec.severity == 'success' %}OK
|
||||
{% else %}Informacja{% endif %}
|
||||
</span>
|
||||
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 4px 0 0 0; line-height: 1.5;">{{ rec.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Benchmarks — comparison with other members #}
|
||||
{% if gbp_benchmarks and gbp_benchmarks.count is defined and gbp_benchmarks.count > 1 %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h2 class="section-title">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
Porownanie z innymi firmami Norda Biznes ({{ gbp_benchmarks.count }})
|
||||
</h2>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
|
||||
<thead>
|
||||
<tr style="border-bottom: 2px solid var(--border);">
|
||||
<th style="text-align: left; padding: 10px; color: var(--text-tertiary); font-weight: 500;">Metryka</th>
|
||||
<th style="text-align: center; padding: 10px; color: var(--text-primary); font-weight: 600;">{{ company.name[:20] }}{% if company.name|length > 20 %}...{% endif %}</th>
|
||||
<th style="text-align: center; padding: 10px; color: var(--text-tertiary); font-weight: 500;">Srednia Norda Biznes</th>
|
||||
<th style="text-align: center; padding: 10px; color: var(--text-tertiary); font-weight: 500;">vs Srednia</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Rating #}
|
||||
{% if places_data.google_rating is not none %}
|
||||
{% set diff_rating = places_data.google_rating - gbp_benchmarks.avg_rating %}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 10px; font-weight: 500;">Ocena Google</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 700;">
|
||||
<span style="color: #f59e0b;">★</span> {{ places_data.google_rating }}
|
||||
</td>
|
||||
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_rating }}</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_rating >= 0 else '#ef4444' }};">
|
||||
{{ '▲' if diff_rating > 0 else '▼' if diff_rating < 0 else '▬' }} {{ '%+.1f'|format(diff_rating) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{# Reviews count #}
|
||||
{% if places_data.google_reviews_count is not none %}
|
||||
{% set diff_reviews = (places_data.google_reviews_count or 0) - gbp_benchmarks.avg_reviews %}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 10px; font-weight: 500;">Liczba opinii</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 700;">{{ places_data.google_reviews_count }}</td>
|
||||
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_reviews }}</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_reviews >= 0 else '#ef4444' }};">
|
||||
{{ '▲' if diff_reviews > 0 else '▼' if diff_reviews < 0 else '▬' }} {{ '%+d'|format(diff_reviews|int) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{# Photos count #}
|
||||
{% if places_data.google_photos_count is not none %}
|
||||
{% set diff_photos = (places_data.google_photos_count or 0) - gbp_benchmarks.avg_photos %}
|
||||
<tr>
|
||||
<td style="padding: 10px; font-weight: 500;">Liczba zdjec</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 700;">{{ places_data.google_photos_count }}</td>
|
||||
<td style="padding: 10px; text-align: center; color: var(--text-secondary);">{{ gbp_benchmarks.avg_photos }}</td>
|
||||
<td style="padding: 10px; text-align: center; font-weight: 600; color: {{ '#10b981' if diff_photos >= 0 else '#ef4444' }};">
|
||||
{{ '▲' if diff_photos > 0 else '▼' if diff_photos < 0 else '▬' }} {{ '%+d'|format(diff_photos|int) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Google Reviews from Places API #}
|
||||
{% if places_data and places_data.google_reviews_data and places_data.google_reviews_data is not mapping %}
|
||||
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-md);">
|
||||
<h2 class="section-title">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
|
||||
</svg>
|
||||
Opinie Google (z Places API)
|
||||
</h2>
|
||||
{% for review in places_data.google_reviews_data[:5] %}
|
||||
<div style="padding: var(--spacing-md); border-bottom: 1px solid var(--border); {% if loop.last %}border-bottom: none;{% endif %}">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-xs);">
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
|
||||
<strong style="font-size: var(--font-size-sm);">{{ review.author_name if review.author_name is defined else (review.get('authorAttribution', {}).get('displayName', 'Anonim') if review is mapping else 'Anonim') }}</strong>
|
||||
<div style="display: flex; gap: 2px;">
|
||||
{% set rev_rating = review.rating if review.rating is defined else (review.get('rating', 0) if review is mapping else 0) %}
|
||||
{% for i in range(5) %}
|
||||
<span style="color: {{ '#f59e0b' if i < rev_rating|int else '#d1d5db' }}; font-size: 14px;">★</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% set rev_time = review.relative_publish_time if review.relative_publish_time is defined else (review.get('relativePublishTimeDescription', '') if review is mapping else '') %}
|
||||
{% if rev_time %}
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">{{ rev_time }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% set rev_text = review.text if review.text is defined and review.text is string else (review.get('text', {}).get('text', '') if review is mapping else '') %}
|
||||
{% if rev_text %}
|
||||
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 0; line-height: 1.5;">{{ rev_text[:300] }}{% if rev_text|length > 300 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Educational Info Section -->
|
||||
<div class="info-section">
|
||||
<div class="info-section-header">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user