feat(audit): Complete remaining display gaps in GBP and Social Media dashboards
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

GBP: Places API data section, audit errors banner, special hours display
Social: Platform comparison table, refresh timestamp, 5-level activity status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-08 13:54:47 +01:00
parent c11e69eb97
commit f459a6fee2
3 changed files with 159 additions and 0 deletions

View File

@ -339,6 +339,18 @@ def gbp_audit_dashboard(slug):
GBPReview.company_id == company.id
).order_by(GBPReview.publish_time.desc()).limit(5).all() if audit else []
# Get Places API enrichment data from CompanyWebsiteAnalysis
places_data = {}
analysis = db.query(CompanyWebsiteAnalysis).filter(
CompanyWebsiteAnalysis.company_id == company.id
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
if analysis:
places_data = {
'primary_type': getattr(analysis, 'google_primary_type', None),
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
'price_level': getattr(analysis, 'google_price_level', None),
}
# If no audit exists, we still render the page (template handles this)
# The user can trigger an audit from the dashboard
@ -351,6 +363,7 @@ def gbp_audit_dashboard(slug):
company=company,
audit=audit,
recent_reviews=recent_reviews,
places_data=places_data,
can_audit=can_audit,
gbp_audit_available=GBP_AUDIT_AVAILABLE,
gbp_audit_version=GBP_AUDIT_VERSION

View File

@ -996,6 +996,44 @@
</div>
</div>
{% if places_data and (places_data.primary_type or places_data.editorial_summary or places_data.price_level) %}
<!-- Google Places Enrichment Data -->
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<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" viewBox="0 0 24 24" fill="#4285f4">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
Dane z Google Places
</h3>
{% 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.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.price_level is not none and places_data.price_level %}
<div style="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' %}Bezplatne
{% elif places_data.price_level == 'PRICE_LEVEL_INEXPENSIVE' %}$ Niedrogi
{% elif places_data.price_level == 'PRICE_LEVEL_MODERATE' %}$$ Umiarkowany
{% elif places_data.price_level == 'PRICE_LEVEL_EXPENSIVE' %}$$$ Drogi
{% elif places_data.price_level == 'PRICE_LEVEL_VERY_EXPENSIVE' %}$$$$ Bardzo drogi
{% else %}{{ places_data.price_level|replace('PRICE_LEVEL_', '')|replace('_', ' ')|title }}
{% endif %}
</span>
</div>
{% endif %}
</div>
{% endif %}
<!-- Fields Section -->
<div class="fields-section">
<h2 class="section-title">
@ -1376,6 +1414,13 @@
<span class="field-name">Godziny specjalne</span>
<span class="field-status-badge {{ 'complete' if audit.has_special_hours else 'partial' }}">{{ 'Ustawione' if audit.has_special_hours else 'Brak' }}</span>
</div>
{% if audit.special_hours %}
<div style="margin-top: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--text-secondary);">
{% for entry in audit.special_hours %}
<div>{{ entry.get('date', '') }}: {% if entry.get('closed') %}Zamkniete{% else %}{{ entry.get('open', '') }} - {{ entry.get('close', '') }}{% endif %}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
@ -1549,6 +1594,14 @@
{% include 'partials/audit_ai_actions.html' %}
{% endwith %}
{% if audit.audit_errors %}
<!-- Audit Errors / Warnings -->
<div style="background: #fef3c7; padding: var(--spacing-md); border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl); border-left: 4px solid #f59e0b;">
<div style="font-size: var(--font-size-sm); font-weight: 600; color: #92400e; margin-bottom: var(--spacing-xs);">Uwagi z audytu</div>
<p style="font-size: var(--font-size-xs); color: #78350f; margin: 0;">{{ audit.audit_errors }}</p>
</div>
{% endif %}
{% else %}
<!-- No Audit State -->
<div class="no-audit-state">

View File

@ -790,12 +790,42 @@
Do weryfikacji
</span>
{% elif profile %}
{% if profile.posting_frequency_score is not none and profile.posting_frequency_score >= 7 %}
<span class="platform-status active">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Aktywny
</span>
{% elif profile.posting_frequency_score is not none and profile.posting_frequency_score >= 4 %}
<span class="platform-status" style="background: rgba(245, 158, 11, 0.1); color: #b45309;">
<svg width="12" height="12" 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>
Umiarkowanie aktywny
</span>
{% elif profile.posting_frequency_score is not none and profile.posting_frequency_score >= 1 %}
<span class="platform-status" style="background: rgba(249, 115, 22, 0.1); color: #c2410c;">
<svg width="12" height="12" 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>
Rzadko aktywny
</span>
{% elif profile.posting_frequency_score is not none and profile.posting_frequency_score == 0 %}
<span class="platform-status" style="background: rgba(239, 68, 68, 0.1); color: #dc2626;">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
Nieaktywny
</span>
{% else %}
<span class="platform-status" style="background: rgba(107, 114, 128, 0.1); color: #6b7280;">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Brak danych
</span>
{% endif %}
{% else %}
<span class="platform-status inactive">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -869,6 +899,14 @@
{{ profile.verified_at.strftime('%d.%m.%Y') }}
</div>
{% endif %}
{% if profile.last_checked_at %}
<div class="platform-meta-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Sprawdzono: {{ profile.last_checked_at.strftime('%d.%m.%Y') }}</span>
</div>
{% endif %}
{% if profile.has_bio %}
<div class="platform-meta-item" title="Ma opis profilu">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -976,6 +1014,61 @@
{% endfor %}
</div>
<!-- Platform Comparison -->
{% set ns = namespace(active_count=0) %}
{% for platform in social_data.all_platforms %}
{% if social_data.profiles.get(platform) and social_data.profiles[platform].is_valid %}
{% set ns.active_count = ns.active_count + 1 %}
{% endif %}
{% endfor %}
{% if ns.active_count > 1 %}
<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 platform
</h2>
<div style="background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden; margin-bottom: var(--spacing-xl);">
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="background: var(--bg-tertiary);">
<th style="padding: var(--spacing-sm) var(--spacing-md); text-align: left; font-weight: 600; color: var(--text-primary);">Platforma</th>
<th style="padding: var(--spacing-sm) var(--spacing-md); text-align: right; font-weight: 600; color: var(--text-primary);">Obserwujacy</th>
<th style="padding: var(--spacing-sm) var(--spacing-md); text-align: right; font-weight: 600; color: var(--text-primary);">Engagement</th>
<th style="padding: var(--spacing-sm) var(--spacing-md); text-align: right; font-weight: 600; color: var(--text-primary);">Posty (30d)</th>
<th style="padding: var(--spacing-sm) var(--spacing-md); text-align: right; font-weight: 600; color: var(--text-primary);">Kompletnosc</th>
</tr>
</thead>
<tbody>
{% for platform in social_data.all_platforms %}
{% set cmp_profile = social_data.profiles.get(platform) %}
{% if cmp_profile and cmp_profile.is_valid %}
<tr style="border-top: 1px solid var(--border);">
<td style="padding: var(--spacing-sm) var(--spacing-md); font-weight: 500;">{{ platform_names.get(platform, platform|title) }}</td>
<td style="padding: var(--spacing-sm) var(--spacing-md); text-align: right;">{{ '{:,}'.format(cmp_profile.followers_count or 0).replace(',', ' ') }}</td>
<td style="padding: var(--spacing-sm) var(--spacing-md); text-align: right;">
{% if cmp_profile.engagement_rate is not none %}
<span style="color: {{ '#10b981' if cmp_profile.engagement_rate >= 3 else ('#f59e0b' if cmp_profile.engagement_rate >= 1 else '#ef4444') }};">{{ '%.1f'|format(cmp_profile.engagement_rate) }}%</span>
{% else %}&mdash;{% endif %}
</td>
<td style="padding: var(--spacing-sm) var(--spacing-md); text-align: right;">{{ cmp_profile.posts_count_30d or 0 }}</td>
<td style="padding: var(--spacing-sm) var(--spacing-md); text-align: right;">
{% if cmp_profile.profile_completeness_score is not none %}
<span style="color: {{ '#10b981' if cmp_profile.profile_completeness_score >= 80 else ('#f59e0b' if cmp_profile.profile_completeness_score >= 50 else '#ef4444') }};">{{ cmp_profile.profile_completeness_score }}%</span>
{% else %}&mdash;{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if social_data.total_platforms - social_data.platforms_count > 0 %}
<!-- Recommendations -->
<h2 class="section-title">