{% extends "base.html" %} {% block title %}Audyt Google Business Profile - {{ company.name }} - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Audyt Google Business Profile

{{ company.name }}

Analiza kompletności wizytówki Google dla lokalnego SEO
Profil firmy {% if audit and (audit.google_maps_url or audit.google_place_id) %} {% set gbp_url = audit.google_maps_url if audit.google_maps_url else 'https://www.google.com/maps/search/?api=1&query=Google&query_place_id=' ~ audit.google_place_id %} Zobacz wizytówkę Google {% endif %} {% if places_data and places_data.maps_links %} {% if places_data.maps_links.writeAReviewUri %} Poproś o opinię {% endif %} {% if places_data.maps_links.directionsUri %} Pokaż trasę {% endif %} {% endif %} {% if can_audit %} {% endif %}
{# GBP Console Connection Status — at the top for visibility #} {% if gbp_connection and gbp_connection.connected %} {% if gbp_connection.is_active and not gbp_connection.is_expired %}
Konsola Google Business Profile połączona

Dostępne są pełne dane: wyświetlenia, wyszukiwania, kliknięcia i interakcje klientów. {% if gbp_connection.google_email %}Konto: {{ gbp_connection.google_email }}.{% endif %} {% if gbp_connection.created_at %}Połączone od {{ gbp_connection.created_at|local_time('%d.%m.%Y') }}.{% endif %}

Zarządzaj
{% else %}
Połączenie z konsolą GBP wygasło

Autoryzacja Google wygasła{% if gbp_connection.expires_at %} {{ gbp_connection.expires_at|local_time('%d.%m.%Y') }}{% endif %}. {% if gbp_connection.google_email %}Ostatnio użyte konto: {{ gbp_connection.google_email }}.{% endif %} Połącz ponownie tym samym kontem, aby przywrócić pełne statystyki.

Połącz ponownie
{% endif %} {% else %}
Konsola Google Business Profile niepołączona

Dane audytu pochodzą wyłącznie z publicznego API Google Places. Po połączeniu konta będą dostępne dodatkowe statystyki: wyświetlenia w Google, kliknięcia, połączenia telefoniczne i nawigacje.

Połącz konto
{% endif %} {% if audit %} {# Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green #} {% set score = audit.completeness_score %}
{{ score }} / 100
{% if places_data and places_data.open_now is not none %}
{% if places_data.open_now %}Otwarte{% else %}Zamknięte{% endif %} (na moment audytu)
{% endif %}
{% if score >= 90 %} Doskonały profil GBP {% elif score >= 70 %} Dobry profil GBP {% elif score >= 50 %} Przeciętny profil GBP {% elif score >= 30 %} Profil wymaga uzupełnienia {% else %} Słaby profil GBP {% endif %}

{% if audit.completeness_score >= 90 %} Twoja wizytówka Google jest bardzo dobrze zoptymalizowana. Utrzymaj wysoki standard i monitoruj opinie klientów. {% elif audit.completeness_score >= 70 %} Profil jest w dobrym stanie, ale są obszary do poprawy. Skupienie się na rekomendacjach zwiększy widoczność. {% elif audit.completeness_score >= 50 %} Wizytówka wymaga uzupełnienia. Wdrożenie poniższych rekomendacji znacząco poprawi lokalne SEO. {% else %} Wizytówka jest niekompletna i traci potencjalnych klientów. Priorytetowo uzupełnij brakujące informacje. {% endif %}

Ostatni audyt: {{ audit.audit_date|local_time('%d.%m.%Y %H:%M') if audit.audit_date else 'Brak danych' }}
{% if audit.review_count %}
{{ audit.review_count }} opinii{% if audit.average_rating %} ({{ audit.average_rating }}/5){% endif %}
{% endif %}
{% 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) %}

Dane z Google Places

{# Business status badge #} {% if places_data.google_business_status %}
{% set bs = places_data.google_business_status %} {% 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 %}
{% endif %} {% if places_data.primary_type %}
Typ: {{ places_data.primary_type_display or places_data.primary_type|replace('_', ' ')|title }}
{% endif %} {% if places_data.google_types and places_data.google_types is iterable and places_data.google_types is not string %}
Wszystkie kategorie: {% for t in places_data.google_types[:8] %} {{ t|replace('_', ' ')|title }} {% endfor %}
{% endif %} {% if places_data.editorial_summary %}
Opis Google: {{ places_data.editorial_summary }}
{% endif %} {% if places_data.google_website %}
Strona WWW: {{ places_data.google_website[:60] }}{% if places_data.google_website|length > 60 %}...{% endif %}
{% endif %} {% if places_data.google_place_id %}
Place ID: {{ places_data.google_place_id }}
{% endif %} {% if places_data.price_level is not none and places_data.price_level %}
Poziom cen: {% if places_data.price_level == 'PRICE_LEVEL_FREE' %}Bezpłatne {% 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 %}
{% endif %} {# Google rating and reviews summary #} {% if places_data.google_rating is not none %}
Ocena: {% for i in range(5) %} {% endfor %} {{ places_data.google_rating }}/5
{% if places_data.google_reviews_count %}
Opinii: {{ places_data.google_reviews_count }}
{% endif %} {% if places_data.google_photos_count %}
Zdjęć: {{ places_data.google_photos_count }}
{% endif %}
{% endif %}
{% 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) %}

Integracja ze stroną WWW

{% if places_data.has_google_analytics is not none %}
{{ '✓' if places_data.has_google_analytics else '✗' }} Google Analytics
{% endif %} {% if places_data.has_google_tag_manager is not none %}
{{ '✓' if places_data.has_google_tag_manager else '✗' }} Google Tag Manager
{% endif %} {% if places_data.has_google_maps_embed is not none %}
{{ '✓' if places_data.has_google_maps_embed else '✗' }} Mapa Google na stronie
{% endif %}
{% endif %} {# Google Attributes #} {% if places_data and places_data.google_attributes and places_data.google_attributes is mapping %}

Atrybuty Google Business

{% for key, value in places_data.google_attributes.items() %} {{ key|replace('_', ' ')|replace('.', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} ✗{% endif %} {% endfor %}
{% endif %} {% if places_data and places_data.google_name %}

Porównanie NAP (Name, Address, Phone)

Spójność danych NAP wpływa na lokalne SEO. Różnice mogą obniżać widoczność w Google.

{% set name_match = (company.name|lower|trim == places_data.google_name|lower|trim) if places_data.google_name else none %} {% set our_addr = ((company.address_street or '') ~ ' ' ~ (company.address_city or ''))|trim %} {% set addr_match = (our_addr|lower in (places_data.google_address|lower) or (places_data.google_address|lower) in our_addr|lower) if (places_data.google_address and our_addr) else none %} {% set phone_clean = (company.phone or '')|replace(' ','')|replace('-','')|replace('+48','') %} {% set gphone_clean = (places_data.google_phone or '')|replace(' ','')|replace('-','')|replace('+48','') %} {% set phone_match = (phone_clean == gphone_clean) if (phone_clean and gphone_clean) else none %}
Pole Nasza baza Google Status
Nazwa {{ company.name or '—' }} {{ places_data.google_name or '—' }} {% if name_match is none %}— {% elif name_match %} {% else %} {% endif %}
Adres {{ our_addr or '—' }} {{ places_data.google_address or '—' }} {% if addr_match is none %}— {% elif addr_match %} {% else %} {% endif %}
Telefon {{ company.phone or '—' }} {{ places_data.google_phone or '—' }} {% if phone_match is none %}— {% elif phone_match %} {% else %} {% endif %}
{% endif %}

Status pól wizytówki

Kompletne
Częściowe
Brakujące
{% set field_icons = { 'name': '', 'address': '', 'phone': '', 'website': '', 'hours': '', 'categories': '', 'photos': '', 'description': '', 'services': '', 'reviews': '' } %} {% set field_names_pl = { 'name': 'Nazwa firmy', 'address': 'Adres', 'phone': 'Telefon', 'website': 'Strona WWW', 'hours': 'Godziny otwarcia', 'categories': 'Kategorie', 'photos': 'Zdjęcia', 'description': 'Opis', 'services': 'Usługi', 'reviews': 'Opinie' } %} {% set status_names = { 'complete': 'Kompletne', 'partial': 'Częściowe', 'missing': 'Brakuje' } %} {% for field_name, field_data in (audit.fields_status or {}).items() %}
{{ field_icons.get(field_name, '')|safe }} {{ field_names_pl.get(field_name, field_name) }} {{ status_names.get(field_data.status, field_data.status) }}
{% if field_data.value %}
{% if field_name == 'hours' and field_data.value is mapping and field_data.value.weekday_text %} {# Translate English day names to Polish and show ALL 7 days #} {% set day_pl = {'Monday': 'Pon', 'Tuesday': 'Wt', 'Wednesday': 'Sr', 'Thursday': 'Czw', 'Friday': 'Pt', 'Saturday': 'Sob', 'Sunday': 'Ndz'} %}
{% for day_hours in field_data.value.weekday_text %} {% set parts = day_hours.split(': ', 1) %} {{ day_pl.get(parts[0], parts[0]) }} {{ parts[1] if parts|length > 1 else day_hours }} {% endfor %}
{% elif field_name == 'hours' and field_data.value is string and 'weekday_text' in field_data.value %} Godziny ustawione {% elif field_name == 'photos' %} {{ field_data.value }} zdjęć w profilu {% elif field_name == 'reviews' %} {{ field_data.value }} {% elif field_name == 'categories' %} {% set cats = field_data.value.split(', ') if field_data.value is string else [field_data.value] %} {% for cat in cats %} {{ cat }} {% endfor %} {% elif field_name == 'description' %} {{ field_data.value[:120] }}{% if field_data.value|length > 120 %}...{% endif %} {% else %} {{ field_data.value }} {% endif %}
{% endif %} {% if field_name == 'reviews' %}
{% if field_data.score == field_data.max_score %} Doskonale! Maksymalna punktacja {% elif field_data.score == 0 %} Popros zadowolonych klientow o opinie w Google {% else %} Dobry poczatek! Zbierz wiecej opinii — cel to minimum 5 {% endif %}
{% endif %}
{{ field_data.score|round(1) }}/{{ field_data.max_score }}
{% endfor %}
{% if audit.review_count and audit.review_count > 0 %}

Analiza opinii

{% if audit.review_response_rate is not none %}
Odpowiedzi na opinie {{ '%.0f'|format(audit.review_response_rate) }}%
{{ audit.reviews_with_response or 0 }} z {{ (audit.reviews_with_response or 0) + (audit.reviews_without_response or 0) }} opinii z odpowiedzią
{% endif %} {% if audit.review_sentiment %} {% set sentiment = audit.review_sentiment %}
Sentyment opinii
{{ sentiment.get('positive', 0) }} pozytywnych {{ sentiment.get('neutral', 0) }} neutralnych {{ sentiment.get('negative', 0) }} negatywnych
{% endif %} {% if audit.reviews_30d is not none %}
Nowe opinie (30 dni) {{ audit.reviews_30d }}
{% endif %} {% if audit.review_keywords %}
Słowa kluczowe z opinii
{% for keyword in audit.review_keywords[:8] %} {{ keyword }} {% endfor %}
{% endif %}
{% if recent_reviews and recent_reviews|length > 0 %}

Ostatnie recenzje

{% for review in recent_reviews %}
{{ review.author_name or 'Anonim' }}
{% for i in range(5) %} {% endfor %}
{% if review.sentiment %} {{ review.sentiment }} {% endif %}
{% if review.publish_time %} {{ review.publish_time|local_time('%d.%m.%Y') }} {% endif %}
{% if review.text %}

{{ review.text[:300] }}{% if review.text|length > 300 %}...{% endif %}

{% endif %} {% if review.has_owner_response and review.owner_response_text %}
Odpowiedz wlasciciela: {% if review.owner_response_time %} {{ review.owner_response_time|local_time('%d.%m.%Y') }} {% endif %}

{{ review.owner_response_text[:200] }}{% if review.owner_response_text|length > 200 %}...{% endif %}

{% endif %} {% if review.keywords %}
{% for kw in review.keywords[:5] %} {{ kw }} {% endfor %}
{% endif %}
{% endfor %}
{% endif %} {% endif %} {% if audit.description_keywords or audit.avg_review_response_days is not none %}

Analiza dodatkowa

{% if audit.avg_review_response_days is not none %} {% set resp_days = audit.avg_review_response_days %}
Średni czas odpowiedzi {{ '%.1f'|format(resp_days) }} dni
{% if resp_days <= 2 %} Doskonały czas reakcji na opinie klientów {% elif resp_days <= 7 %} Dobry czas reakcji — postaraj się odpowiadać w ciągu 1-2 dni {% else %} Długi czas odpowiedzi — klienci oczekują szybszej reakcji {% endif %}
{% endif %} {% if audit.description_keywords %}
Słowa kluczowe w opisie
{% for keyword in audit.description_keywords[:10] %} {{ keyword }} {% endfor %}
{% if audit.keyword_density_score is not none %}
Gęstość słów kluczowych {{ audit.keyword_density_score }}/10
{% endif %}
{% endif %}
{% endif %} {% if audit.nap_consistent is not none %}

Spójność NAP (Nazwa / Adres / Telefon)

{% if audit.nap_consistent %}
Dane NAP na wizytówce Google są spójne z danymi na stronie WWW firmy.
{% else %}
Wykryto różnice między wizytówką Google a stroną WWW firmy.
{% if audit.nap_issues %}
{% for issue in audit.nap_issues %}
{{ issue.field|capitalize }} Rozbieżność
Google: {{ issue.gbp or 'Brak' }}
Strona WWW: {{ issue.website or 'Brak' }}
{% endfor %}
{% endif %} {% endif %}
{% endif %} {% if audit.has_posts is not none or audit.attributes %}

Aktywność i atrybuty

{% if audit.has_posts is not none %}
Posty w Google {{ 'Aktywne' if audit.has_posts else 'Brak' }}
{% if audit.posts_count_30d %}
{{ audit.posts_count_30d }} postów w ostatnich 30 dniach
{% endif %}
{% endif %} {% if audit.has_qa is not none %}
Pytania i odpowiedzi {{ audit.qa_count or 0 }}
{% endif %} {% if audit.has_products is not none %}
Produkty / Menu {{ 'Dodane' if audit.has_products else 'Brak' }}
{% endif %} {% if audit.has_special_hours is not none %}
Godziny specjalne {{ 'Ustawione' if audit.has_special_hours else 'Brak' }}
{% if audit.special_hours %}
{% for entry in audit.special_hours %}
{{ entry.get('date', '') }}: {% if entry.get('closed') %}Zamknięte{% else %}{{ entry.get('open', '') }} - {{ entry.get('close', '') }}{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% if audit.logo_present is not none or audit.cover_photo_present is not none %}
{% if audit.logo_present is not none %}
{{ '✓' if audit.logo_present else '✗' }} Logo
{% endif %} {% if audit.cover_photo_present is not none %}
{{ '✓' if audit.cover_photo_present else '✗' }} Zdjęcie w tle
{% endif %}
{% endif %} {% if audit.photo_categories %}

Kategorie zdjęć

{% for category, count in audit.photo_categories.items() %} {{ category|capitalize }}: {{ count }} {% endfor %}
{% endif %} {% if audit.attributes %}

Atrybuty Google Business

{% for key, value in audit.attributes.items() %} {{ key|replace('_', ' ')|title }}{% if value is string %}: {{ value }}{% elif not value %} ✗{% endif %} {% endfor %}
{% endif %}
{% endif %} {% if places_data and places_data.gbp_impressions_maps is not none %}

Statystyki widocznosci ({{ places_data.gbp_performance_period_days or 30 }} dni)

Wyswietlenia profilu

{{ '{:,}'.format(places_data.gbp_impressions_maps or 0) }}
Google Maps
{{ '{:,}'.format(places_data.gbp_impressions_search or 0) }}
Wyszukiwarka Google
{{ '{:,}'.format((places_data.gbp_impressions_maps or 0) + (places_data.gbp_impressions_search or 0)) }}
Lacznie

Akcje klientow

{{ places_data.gbp_call_clicks or 0 }}
Klikniecia telefon
{{ places_data.gbp_website_clicks or 0 }}
Klikniecia strona
{{ places_data.gbp_direction_requests or 0 }}
Prosby o trase
{% if places_data.gbp_conversations %}
{{ places_data.gbp_conversations }}
Rozmowy
{% endif %}
{% if places_data.gbp_search_keywords %}

Frazy wyszukiwania

{% for kw in places_data.gbp_search_keywords[:10] %} {% endfor %}
# Fraza Wyswietlenia
{{ loop.index }} {{ kw.keyword }} {{ '{:,}'.format(kw.impressions) }}
{% endif %}
{% endif %} {% if places_data and places_data.google_posts_data %}

Google Posts ({{ places_data.google_posts_count or places_data.google_posts_data|length }})

{% for post in places_data.google_posts_data[:5] %}
{{ post.topicType|default(post.get('searchUrl', 'POST')|default('Post'))|replace('_', ' ')|title }} {% if post.createTime or post.get('createTime') %} {{ (post.createTime or post.get('createTime', ''))[:10] }} {% endif %}
{% set summary = post.get('summary', post.get('text', '')) %} {% if summary %}

{{ summary[:200] }}{% if summary|length > 200 %}...{% endif %}

{% endif %}
{% endfor %}
{% endif %} {% if places_data and places_data.google_owner_responses_count is not none %}

Odpowiedzi na opinie

{{ places_data.google_owner_responses_count }}
Odpowiedzi wlasciciela
{% if places_data.google_review_response_rate is not none %}
{{ places_data.google_review_response_rate }}%
Wskaznik odpowiedzi
{% endif %}
{% endif %} {% if audit.recommendations %}

Rekomendacje ({{ audit.recommendations|length }})

{% for rec in audit.recommendations %}
{% if rec.priority == 'high' %} {% elif rec.priority == 'medium' %} {% else %} {% endif %}
{{ field_names_pl.get(rec.field, rec.field) }}
{{ rec.recommendation }}
+{{ rec.impact }} pkt
{% endfor %}
{% endif %} {# Smart Recommendations (from backend analysis) #} {% if gbp_recommendations and gbp_recommendations|length > 0 %}

Automatyczne zalecenia

{% for rec in gbp_recommendations %}
{% if rec.severity == 'critical' %}⚠ {% elif rec.severity == 'warning' %}⚠ {% elif rec.severity == 'success' %}✔ {% else %}ℹ{% endif %}
{% if rec.severity == 'critical' %}Krytyczne {% elif rec.severity == 'warning' %}Zalecenie {% elif rec.severity == 'success' %}OK {% else %}Informacja{% endif %}

{{ rec.text }}

{% endfor %}
{% endif %} {# Benchmarks — comparison with other members #} {% if gbp_benchmarks and gbp_benchmarks.count is defined and gbp_benchmarks.count > 1 %}

Porownanie z innymi firmami Norda Biznes ({{ gbp_benchmarks.count }})

{# Rating #} {% if places_data.google_rating is not none %} {% set diff_rating = places_data.google_rating - gbp_benchmarks.avg_rating %} {% 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 %} {% 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 %} {% endif %}
Metryka {{ company.name[:20] }}{% if company.name|length > 20 %}...{% endif %} Srednia Norda Biznes vs Srednia
Ocena Google {{ places_data.google_rating }} {{ gbp_benchmarks.avg_rating }} {{ '▲' if diff_rating > 0 else '▼' if diff_rating < 0 else '—' }} {{ '%+.1f'|format(diff_rating) }}
Liczba opinii {{ places_data.google_reviews_count }} {{ gbp_benchmarks.avg_reviews }} {{ '▲' if diff_reviews > 0 else '▼' if diff_reviews < 0 else '—' }} {{ '%+d'|format(diff_reviews|int) }}
Liczba zdjec {{ places_data.google_photos_count }} {{ gbp_benchmarks.avg_photos }} {{ '▲' if diff_photos > 0 else '▼' if diff_photos < 0 else '—' }} {{ '%+d'|format(diff_photos|int) }}
{% endif %} {# Google Reviews from Places API — data is dict with 'reviews' key #} {% if places_data and places_data.google_reviews_data is mapping and places_data.google_reviews_data.get('reviews') %} {% set api_reviews = places_data.google_reviews_data.reviews %}

Opinie z Google Places API ({{ places_data.google_reviews_data.get('total_from_api', api_reviews|length) }} z {{ places_data.google_reviews_data.get('total_reported', '?') }})

{# Rating distribution bar #} {% if places_data.google_reviews_data.get('rating_distribution') %} {% set rd = places_data.google_reviews_data.rating_distribution %} {# JSONB stores keys as strings, so look up both int and string keys #} {% set rd_total = rd.values()|sum %} {% if rd_total > 0 %}
{% for star in [5, 4, 3, 2, 1] %} {% set star_count = rd.get(star, rd.get(star|string, 0)) %}
{{ star }}★
{{ star_count }}
{% endfor %}
{% endif %} {% endif %} {# Response rate from Places API data #} {% if places_data.google_reviews_data.get('response_rate') is not none %}
Odpowiedzi na opinie: {{ places_data.google_reviews_data.with_response|default(0) }} z {{ (places_data.google_reviews_data.with_response|default(0)) + (places_data.google_reviews_data.without_response|default(0)) }} ({{ '%.0f'|format(places_data.google_reviews_data.response_rate) }}%)
{% endif %} {# Individual reviews #} {% for review in api_reviews[:5] %}
{{ review.author|default('Anonim') }}
{% for i in range(5) %} {% endfor %}
{% if review.relative_time %} {{ review.relative_time }} {% endif %}
{% if review.text %}

{{ review.text[:300] }}{% if review.text|length > 300 %}...{% endif %}

{% endif %}
{% endfor %}
{% endif %} {# Google Photos Metadata #} {% if places_data and places_data.google_photos_metadata is mapping and places_data.google_photos_metadata.get('photos') %} {% set photos_meta = places_data.google_photos_metadata %}

Zdjecia w Google ({{ photos_meta.get('total_count', photos_meta.photos|length) }})

{% if photos_meta.get('has_owner_photos') is not none %}
{% if photos_meta.has_owner_photos %}✓ Wlasciciel dodal zdjecia{% else %}✗ Brak zdjec od wlasciciela{% endif %}
{% endif %}
{% for photo in photos_meta.photos[:10] %} {% endfor %}
# Autor Wymiary
{{ loop.index }} {{ photo.get('attribution', '—') }} {% if photo.get('width') and photo.get('height') %}{{ photo.width }}×{{ photo.height }}{% else %}—{% endif %}
{% endif %}

Jak działa wizytówka Google?

Wyszukiwarka Google

Gdy ktoś szuka Twojej firmy w Google, po prawej stronie wyników pojawia się Panel Wiedzy (Knowledge Panel).

  • Nazwa i logo firmy
  • Adres i godziny otwarcia
  • Ocena i opinie
  • Przycisk "Zadzwoń" i "Trasa"

Mapy Google

W aplikacji Google Maps Twoja firma ma pełną wizytówkę z dodatkowymi funkcjami.

  • Zdjęcia i wirtualny spacer
  • Wszystkie opinie klientów
  • Pytania i odpowiedzi (Q&A)
  • Posty i aktualności firmy

Jak zarządzać?

Wszystkie dane edytujesz w jednym miejscu — panelu Google Business Profile.

  • Wejdz na business.google.com
  • Zaloguj się kontem Google
  • Edytuj dane — zaktualizują się wszędzie
  • Odpowiadaj na opinie klientów

Jedno źródło, wiele widoków. Panel Wiedzy w wyszukiwarce i wizytówka w Mapach Google to te same dane wyświetlane w różnych miejscach. Wystarczy, że zarządzasz profilem w Google Business Profile — zmiany automatycznie pojawią się wszędzie: w wyszukiwarce, Mapach, Asystencie Google i wynikach lokalnych.

{% with audit_type='gbp' %} {% include 'partials/audit_ai_actions.html' %} {% endwith %} {# Audit Diagnostics #} {% if audit.audit_errors or audit.audit_source or audit.audit_version %}
{% if audit.audit_source %} Zrodlo: {{ audit.audit_source }} {% endif %} {% if audit.audit_version %} Wersja: {{ audit.audit_version }} {% endif %} {% if audit.audit_date %} Data: {{ audit.audit_date|local_time('%d.%m.%Y %H:%M') }} {% endif %}
{% if audit.audit_errors %}
Uwagi: {{ audit.audit_errors }}
{% endif %}
{% endif %} {% else %}

Brak danych audytu

Nie przeprowadzono jeszcze audytu wizytówki Google dla tej firmy. Uruchom audyt, aby sprawdzić kompletność profilu.

{% if can_audit %} {% endif %}
{% endif %}

Audyt Google Business Profile

Pobieram dane z Google i analizuję wizytówkę...

Szukam firmy w Google Maps...
Ocena Google
Liczba opinii Google
Zdjęcia Google
Godziny otwarcia Google
Numer telefonu Google
Strona WWW Google
Status firmy Google
Zapisuje dane w bazie
Analizuję kompletność profilu
{% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; const companySlug = '{{ company.slug }}'; // All step IDs in order const allSteps = [ 'step-find', 'step-rating', 'step-reviews', 'step-photos', 'step-hours', 'step-phone', 'step-website', 'step-status', 'step-save', 'step-audit' ]; // Detail step labels (defaults) with source tags const detailLabels = { 'step-rating': 'Ocena Google', 'step-reviews': 'Liczba opinii Google', 'step-photos': 'Zdjęcia Google', 'step-hours': 'Godziny otwarcia Google', 'step-phone': 'Numer telefonu Google', 'step-website': 'Strona WWW Google', 'step-status': 'Status firmy Google' }; // SVG icons for different states const icons = { pending: '', in_progress: '
', complete: '', error: '', warning: '', skipped: '', missing: '' }; function resetSteps() { // Reset all steps to default labels and pending state allSteps.forEach((stepId, index) => { const stepEl = document.getElementById(stepId); if (stepEl) { const iconEl = stepEl.querySelector('.step-icon'); const textEl = stepEl.querySelector('.step-text'); // Reset label to default (use innerHTML to preserve source tags) if (detailLabels[stepId]) { textEl.innerHTML = detailLabels[stepId]; } if (index === 0) { iconEl.className = 'step-icon in_progress'; iconEl.innerHTML = icons.in_progress; textEl.className = 'step-text in_progress'; } else { iconEl.className = 'step-icon pending'; iconEl.innerHTML = icons.pending; textEl.className = 'step-text pending'; } } }); } function updateStep(stepId, status, message) { const stepEl = document.getElementById(stepId); if (!stepEl) return; const iconEl = stepEl.querySelector('.step-icon'); const textEl = stepEl.querySelector('.step-text'); iconEl.className = 'step-icon ' + status; iconEl.innerHTML = icons[status] || icons.pending; textEl.className = 'step-text ' + status; if (message) { textEl.innerHTML = message; } } function showLoading() { resetSteps(); document.getElementById('loadingOverlay').classList.add('active'); } function hideLoading() { document.getElementById('loadingOverlay').classList.remove('active'); } function showInfoModal(title, body, isSuccess) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalBody').textContent = body; const icon = document.getElementById('modalIcon'); icon.className = 'modal-icon ' + (isSuccess ? 'success' : 'info'); document.getElementById('infoModal').classList.add('active'); } function closeInfoModal() { document.getElementById('infoModal').classList.remove('active'); } document.getElementById('infoModal')?.addEventListener('click', (e) => { if (e.target.id === 'infoModal') closeInfoModal(); }); // Update detail steps with fetched data values async function updateDetailSteps(googleData) { const delay = 150; // ms between each step animation // Rating updateStep('step-rating', 'in_progress', 'Pobieram ocenę...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_rating) { updateStep('step-rating', 'complete', `Ocena: ${googleData.google_rating}/5`); } else { updateStep('step-rating', 'missing', 'Brak oceny'); } // Reviews updateStep('step-reviews', 'in_progress', 'Pobieram opinie...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_reviews_count) { updateStep('step-reviews', 'complete', `Opinie: ${googleData.google_reviews_count}`); } else { updateStep('step-reviews', 'missing', 'Brak opinii'); } // Photos updateStep('step-photos', 'in_progress', 'Pobieram zdjęcia...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_photos_count) { updateStep('step-photos', 'complete', `Zdjęcia: ${googleData.google_photos_count}`); } else { updateStep('step-photos', 'missing', 'Brak zdjęć'); } // Opening hours updateStep('step-hours', 'in_progress', 'Pobieram godziny otwarcia...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_opening_hours && googleData.google_opening_hours.weekday_text) { updateStep('step-hours', 'complete', 'Godziny otwarcia: ustawione'); } else { updateStep('step-hours', 'missing', 'Brak godzin otwarcia'); } // Phone updateStep('step-phone', 'in_progress', 'Pobieram telefon...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_phone) { updateStep('step-phone', 'complete', `Telefon: ${googleData.google_phone}`); } else { updateStep('step-phone', 'missing', 'Brak telefonu'); } // Website updateStep('step-website', 'in_progress', 'Pobieram stronę WWW...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_website) { // Truncate long URLs const shortUrl = googleData.google_website.replace(/^https?:\/\//, '').slice(0, 30); updateStep('step-website', 'complete', `WWW: ${shortUrl}...`); } else { updateStep('step-website', 'missing', 'Brak strony WWW'); } // Business status updateStep('step-status', 'in_progress', 'Pobieram status...'); await new Promise(r => setTimeout(r, delay)); if (googleData.google_business_status) { const statusMap = { 'OPERATIONAL': 'Czynna', 'CLOSED_TEMPORARILY': 'Tymczasowo zamknięta', 'CLOSED_PERMANENTLY': 'Zamknięta na stałe' }; const statusText = statusMap[googleData.google_business_status] || googleData.google_business_status; updateStep('step-status', 'complete', `Status: ${statusText}`); } else { updateStep('step-status', 'missing', 'Brak statusu'); } } async function runAudit() { const btn = document.getElementById('runAuditBtn'); if (btn) { btn.disabled = true; } showLoading(); // Simulate step animation start await new Promise(r => setTimeout(r, 300)); try { const response = await fetch('/api/gbp/audit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ slug: companySlug, force_refresh: true }) }); const data = await response.json(); // Handle find_place step if (data.google_fetch && data.google_fetch.steps) { const findStep = data.google_fetch.steps.find(s => s.step === 'find_place'); if (findStep) { updateStep('step-find', findStep.status, findStep.message); } } // If we have Google data, animate the detail steps with actual values if (data.google_fetch && data.google_fetch.data && data.google_fetch.success) { await updateDetailSteps(data.google_fetch.data); // Save step const saveStep = data.google_fetch.steps.find(s => s.step === 'save_data'); if (saveStep) { updateStep('step-save', saveStep.status, saveStep.message); } } else if (data.google_fetch && data.google_fetch.steps) { // Mark detail steps as skipped if no Google data const detailStepIds = ['step-rating', 'step-reviews', 'step-photos', 'step-hours', 'step-phone', 'step-website', 'step-status']; for (const stepId of detailStepIds) { updateStep(stepId, 'skipped', detailLabels[stepId] + ' (pominięty)'); } } // Update audit step if (response.ok && data.success) { updateStep('step-save', 'complete', 'Dane zapisane'); updateStep('step-audit', 'complete', `Analiza zakończona: ${data.audit?.total_score || 0}/100`); // Wait 5 seconds so user can read the progress steps await new Promise(r => setTimeout(r, 5000)); hideLoading(); showInfoModal('Audyt zakończony', 'Audyt wizytówki Google został zakończony pomyślnie. Strona zostanie odświeżona.', true); setTimeout(() => location.reload(), 1500); } else { updateStep('step-audit', 'error', 'Błąd audytu'); // Wait 5 seconds so user can see what failed await new Promise(r => setTimeout(r, 5000)); hideLoading(); showInfoModal('Błąd', data.error || 'Wystąpił nieznany błąd podczas audytu.', false); if (btn) btn.disabled = false; } } catch (error) { hideLoading(); showInfoModal('Błąd połączenia', 'Nie udało się połączyć z serwerem: ' + error.message, false); if (btn) btn.disabled = false; } } /* ============================================================ AI AUDIT ACTIONS ============================================================ */ const companyId = {{ company.id }}; const auditType = 'gbp'; function simpleMarkdown(text) { return text .replace(/&/g, '&').replace(//g, '>') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^- (.+)$/gm, '
  • $1
  • ') .replace(/(
  • .*<\/li>)/gs, '') .replace(/\n/g, '
    '); } async function runAIAnalysis(force) { const prompt = document.getElementById('aiAnalyzePrompt'); const loading = document.getElementById('aiLoading'); const results = document.getElementById('aiResults'); const btn = document.getElementById('aiAnalyzeBtn'); if (btn) btn.disabled = true; if (prompt) prompt.style.display = 'none'; if (results) results.style.display = 'none'; if (loading) loading.style.display = 'block'; let seconds = 0; const timerEl = document.getElementById('aiTimer'); const timerInterval = setInterval(() => { seconds++; if (timerEl) timerEl.textContent = seconds + 's'; }, 1000); try { const response = await fetch('/api/audit/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ company_id: companyId, audit_type: auditType, force: !!force }) }); const data = await response.json(); clearInterval(timerInterval); if (loading) loading.style.display = 'none'; if (data.success) { renderAIResults(data); } else { if (prompt) prompt.style.display = 'none'; if (btn) btn.disabled = false; const results = document.getElementById('aiResults'); results.innerHTML = `

    Błąd analizy AI

    ${escapeHtml(data.error || 'Nieznany błąd')}

    `; results.style.display = 'block'; } } catch (error) { clearInterval(timerInterval); if (loading) loading.style.display = 'none'; if (prompt) prompt.style.display = 'none'; if (btn) btn.disabled = false; const results = document.getElementById('aiResults'); results.innerHTML = `

    Błąd połączenia

    ${escapeHtml(error.message)}

    `; results.style.display = 'block'; } } function renderAIResults(data) { const results = document.getElementById('aiResults'); const summaryEl = document.getElementById('aiSummaryText'); const cacheInfo = document.getElementById('aiCacheInfo'); const actionsList = document.getElementById('aiActionsList'); summaryEl.textContent = data.summary || ''; if (data.cached && data.generated_at) { const d = new Date(data.generated_at); document.getElementById('aiCacheDate').textContent = d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'}); cacheInfo.style.display = 'block'; } else { cacheInfo.style.display = 'none'; } actionsList.innerHTML = ''; const actions = data.actions || []; const priorityLabels = {critical: 'KRYTYCZNE', high: 'WYSOKI', medium: 'ŚREDNI', low: 'NISKI'}; actions.forEach((action, idx) => { const impact = action.impact_score || 5; const effort = action.effort_score || 5; const card = document.createElement('div'); card.className = 'ai-action-card priority-' + (action.priority || 'medium'); card.id = 'ai-action-' + idx; card.innerHTML = `
    ${priorityLabels[action.priority] || 'ŚREDNI'} ${escapeHtml(action.title || '')}

    ${escapeHtml(action.description || '')}

    Wpływ: ${impact}/10
    Wysiłek: ${effort}/10
    `; actionsList.appendChild(card); }); // Render comparison with previous analysis if available if (typeof renderAIComparison === 'function') renderAIComparison(data); results.style.display = 'block'; document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'}); window._aiActions = actions; } async function generateContent(actionType, idx) { const container = document.getElementById('ai-content-' + idx); if (!container) return; if (container.dataset.loaded === 'true') { container.style.display = container.style.display === 'none' ? 'block' : 'none'; return; } container.innerHTML = '
    Generowanie treści...
    '; container.style.display = 'block'; try { const response = await fetch('/api/audit/generate-content', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken}, body: JSON.stringify({company_id: companyId, action_type: actionType, context: {}}) }); const data = await response.json(); if (data.success && data.content) { const isCode = data.content.includes('{') && (data.content.includes('${escapeHtml(data.content)}`; } else { container.innerHTML = `
    ${simpleMarkdown(data.content)}
    `; } container.dataset.loaded = 'true'; container.scrollIntoView({behavior: 'smooth', block: 'nearest'}); } else { container.innerHTML = `
    ${escapeHtml(data.error || 'Błąd generowania')}
    `; } } catch (error) { container.innerHTML = `
    ${escapeHtml(error.message)}
    `; } } function copyContent(btn) { const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content'); if (!code) return; navigator.clipboard.writeText(code.textContent).then(() => { const orig = btn.textContent; btn.textContent = 'Skopiowano!'; setTimeout(() => { btn.textContent = orig; }, 2000); }); } function markAction(idx, status) { const card = document.getElementById('ai-action-' + idx); if (!card) return; if (status === 'implemented') card.classList.add('implemented'); else if (status === 'dismissed') card.classList.add('dismissed'); const actions = window._aiActions || []; if (actions[idx] && actions[idx].id) { fetch('/api/audit/actions/' + actions[idx].id + '/status', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken}, body: JSON.stringify({status: status}) }).catch(() => {}); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } {% endblock %}