feat(audit): Enhance GBP and Social Media dashboard displays
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: Add recent reviews widget (5 reviews with ratings, sentiment, owner
responses), photo status indicators (logo/cover), business attributes
section (payment, parking, accessibility tags).

Social Media: Show all check_status types (404, blocked, redirect), add
profile source indicator (website scrape/search/manual), add followers
history trend visualization with CSS bars.

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

View File

@ -256,6 +256,7 @@ def social_audit_dashboard(slug):
'content_types': profile.content_types,
'profile_completeness_score': profile.profile_completeness_score,
'followers_history': profile.followers_history,
'source': profile.source,
}
# Calculate score (platforms with profiles / total platforms)

View File

@ -1183,50 +1183,44 @@
</div>
{% if recent_reviews and recent_reviews|length > 0 %}
<!-- Recent Reviews Section -->
<div class="fields-section">
<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 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
Ostatnie opinie ({{ recent_reviews|length }})
</h2>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for review in recent_reviews %}
<div class="field-card {{ 'complete' if review.sentiment == 'positive' else ('partial' if review.sentiment == 'neutral' else 'missing') }}">
<div class="field-header">
<span class="field-name">
{% for i in range(review.rating) %}
<span style="color: #f59e0b;">&#9733;</span>
<!-- Recent Reviews -->
<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>
Ostatnie recenzje
</h2>
<div style="background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden; margin-bottom: var(--spacing-xl);">
{% for review in recent_reviews %}
<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 or 'Anonim' }}</strong>
<div style="display: flex; gap: 2px;">
{% for i in range(5) %}
<span style="color: {{ '#f59e0b' if i < review.rating else '#d1d5db' }}; font-size: 14px;">&#9733;</span>
{% endfor %}
{% for i in range(5 - review.rating) %}
<span style="color: #d1d5db;">&#9733;</span>
{% endfor %}
</span>
<div style="display: flex; align-items: center; gap: var(--spacing-xs);">
{% if review.sentiment %}
<span class="field-status-badge {{ 'complete' if review.sentiment == 'positive' else ('partial' if review.sentiment == 'neutral' else 'missing') }}">
{{ 'Pozytywna' if review.sentiment == 'positive' else ('Neutralna' if review.sentiment == 'neutral' else 'Negatywna') }}
</span>
{% endif %}
{% if review.has_owner_response %}
<span style="display: inline-flex; align-items: center; gap: 2px; font-size: 11px; padding: 2px 8px; background: #dbeafe; color: #1e40af; border-radius: var(--radius-sm);">
<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="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
Odpowiedz
</span>
{% endif %}
</div>
{% if review.sentiment %}
<span style="font-size: var(--font-size-xs); padding: 1px 6px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if review.sentiment == 'positive' else ('#fef3c7' if review.sentiment == 'neutral' else '#fee2e2') }}; color: {{ '#10b981' if review.sentiment == 'positive' else ('#f59e0b' if review.sentiment == 'neutral' else '#ef4444') }};">{{ review.sentiment }}</span>
{% endif %}
</div>
<div style="display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: var(--spacing-xs);">
<span>{{ review.author_name or 'Anonim' }}</span>
<span>{{ review.publish_time.strftime('%d.%m.%Y') if review.publish_time else '' }}</span>
</div>
{% if review.text %}
<div class="field-value">{{ review.text[:200] }}{% if review.text|length > 200 %}...{% endif %}</div>
{% if review.publish_time %}
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">{{ review.publish_time.strftime('%d.%m.%Y') }}</span>
{% endif %}
</div>
{% endfor %}
{% if review.text %}
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 0 0 var(--spacing-xs) 0; line-height: 1.5;">{{ review.text[:300] }}{% if review.text|length > 300 %}...{% endif %}</p>
{% endif %}
{% if review.has_owner_response and review.owner_response_text %}
<div style="margin-top: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-tertiary); border-left: 3px solid var(--primary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-primary); margin-bottom: 2px;">Odpowiedz wlasciciela:</div>
<p style="font-size: var(--font-size-xs); color: var(--text-secondary); margin: 0; line-height: 1.4;">{{ review.owner_response_text[:200] }}{% if review.owner_response_text|length > 200 %}...{% endif %}</p>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
@ -1386,6 +1380,23 @@
{% endif %}
</div>
{% if audit.logo_present is not none or audit.cover_photo_present is not none %}
<div style="display: flex; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); margin-top: var(--spacing-lg);">
{% if audit.logo_present 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 audit.logo_present else '#fee2e2' }};">
<span style="color: {{ '#10b981' if audit.logo_present else '#ef4444' }};">{{ '&#10003;' if audit.logo_present else '&#10007;' }}</span>
<span style="font-size: var(--font-size-sm);">Logo</span>
</div>
{% endif %}
{% if audit.cover_photo_present 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 audit.cover_photo_present else '#fee2e2' }};">
<span style="color: {{ '#10b981' if audit.cover_photo_present else '#ef4444' }};">{{ '&#10003;' if audit.cover_photo_present else '&#10007;' }}</span>
<span style="font-size: var(--font-size-sm);">Zdjecie w tle</span>
</div>
{% endif %}
</div>
{% endif %}
{% if audit.photo_categories %}
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm);">Kategorie zdjec</h3>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
@ -1396,6 +1407,23 @@
{% endfor %}
</div>
{% endif %}
{% if audit.attributes %}
<!-- Business Attributes -->
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm); display: flex; align-items: center; gap: var(--spacing-xs);">
<svg width="18" height="18" 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 audit.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('_', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} &#10007;{% endif %}
</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}

View File

@ -761,27 +761,41 @@
{% endif %}
</div>
<span class="platform-name">{{ platform_names.get(platform, platform|title) }}</span>
{% if profile and profile.check_status == 'needs_verification' %}
{% if profile and profile.check_status == '404' %}
<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>
Profil nie istnieje (404)
</span>
{% elif profile and profile.check_status == 'blocked' %}
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
Zablokowany
</span>
{% elif profile and profile.check_status == 'redirect' %}
<span class="platform-status needs-verification">
<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="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
Przekierowanie
</span>
{% elif profile and profile.check_status == 'needs_verification' %}
<span class="platform-status needs-verification">
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
Do weryfikacji
</span>
{% elif profile and profile.last_post_date and (now - profile.last_post_date).days < 90 %}
{% elif profile %}
<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 %}
<span class="platform-status" style="background: var(--bg-secondary); color: var(--text-secondary);">
<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>
Znaleziony
</span>
{% else %}
<span class="platform-status inactive">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -809,6 +823,14 @@
<strong>Adres profilu zawiera numeryczne ID</strong> zamiast nazwy firmy. Zalecamy ustawienie niestandardowej nazwy uzytkownika (np. facebook.com/NazwaFirmy) w ustawieniach strony na Facebooku.
</div>
{% endif %}
{% if profile.source %}
<div style="display: flex; align-items: center; gap: var(--spacing-xs); margin-bottom: var(--spacing-xs);">
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Zrodlo:</span>
<span style="font-size: var(--font-size-xs); padding: 1px 6px; border-radius: var(--radius-sm); background: #f3f4f6; color: #6b7280;">
{% if profile.source == 'website_scrape' %}Ze strony WWW{% elif profile.source == 'brave_search' %}Wyszukiwarka{% elif profile.source == 'manual' %}Recznie{% elif profile.source == 'facebook_api' %}Facebook API{% else %}{{ profile.source }}{% endif %}
</span>
</div>
{% endif %}
<div class="platform-meta">
{% if profile.page_name %}
<div class="platform-meta-item">
@ -826,6 +848,19 @@
{{ '{:,}'.format(profile.followers_count).replace(',', ' ') }} obserwujacych
</div>
{% endif %}
{% if profile.followers_history and profile.followers_history|length > 1 %}
<div class="platform-meta-item" style="flex-basis: 100%;">
<div style="width: 100%;">
<span style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Trend:</span>
<div style="display: flex; align-items: flex-end; gap: 2px; height: 24px; margin-top: 2px;">
{% set max_val = profile.followers_history|map(attribute='count')|select('number')|max|default(1) %}
{% for point in profile.followers_history[-12:] %}
<div style="width: 6px; background: var(--primary); border-radius: 1px; opacity: 0.7; height: {{ ((point.count|default(0) / max_val) * 100)|int if max_val else 0 }}%;" title="{{ point.date|default('?') }}: {{ point.count|default(0) }}"></div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if profile.verified_at %}
<div class="platform-meta-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">