feat(gbp): Add review link, directions, open status, and NAP comparison
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 4 quick-win features to GBP dashboard:
- "Poproś o opinię" button with writeAReviewUri from Places API
- "Pokaż trasę" button with directionsUri
- Open/Closed badge showing business status at audit time
- NAP comparison table (Name, Address, Phone) vs Google data

New DB columns: google_maps_links (JSONB), google_open_now (BOOLEAN)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-08 15:04:49 +01:00
parent 137f45d0c6
commit ca051be435
6 changed files with 109 additions and 1 deletions

View File

@ -349,6 +349,11 @@ def gbp_audit_dashboard(slug):
'primary_type': getattr(analysis, 'google_primary_type', None),
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
'price_level': getattr(analysis, 'google_price_level', None),
'maps_links': getattr(analysis, 'google_maps_links', None),
'open_now': getattr(analysis, 'google_open_now', None),
'google_name': analysis.google_name,
'google_address': analysis.google_address,
'google_phone': analysis.google_phone,
}
# If no audit exists, we still render the page (template handles this)

View File

@ -1154,6 +1154,8 @@ class CompanyWebsiteAnalysis(Base):
google_attributes = Column(JSONB) # Places API: business attributes
google_reviews_data = Column(JSONB) # Places API: reviews with text/rating
google_photos_metadata = Column(JSONB) # Places API: photo references
google_maps_links = Column(JSONB) # Places API: directionsUri, writeAReviewUri, etc.
google_open_now = Column(Boolean) # Whether business is currently open (at audit time)
# === SEO AUDIT METADATA ===
seo_audit_version = Column(String(20)) # Version of SEO audit script used

View File

@ -0,0 +1,5 @@
-- Migration 062: Add Google Maps links and open_now status
-- Part of GBP Dashboard Quick Wins feature
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS google_maps_links JSONB;
ALTER TABLE company_website_analysis ADD COLUMN IF NOT EXISTS google_open_now BOOLEAN;

View File

@ -1837,6 +1837,9 @@ def fetch_google_business_data(
business_status = place_data.get('businessStatus', '')
editorial_summary = place_data.get('editorialSummary', {}).get('text', '')
price_level = place_data.get('priceLevel', '')
maps_links = place_data.get('googleMapsLinks', {})
current_hours = place_data.get('currentOpeningHours', {})
open_now = current_hours.get('openNow')
# Extract rich data using service methods
reviews_data = places_service.extract_reviews_data(place_data)
@ -1876,6 +1879,8 @@ def fetch_google_business_data(
'google_reviews_data': reviews_data,
'google_photos_metadata': photos_meta,
'google_has_special_hours': hours_data.get('has_special_hours', False),
'google_maps_links': maps_links if maps_links else None,
'google_open_now': open_now,
})
result['steps'][-1]['status'] = 'complete'
@ -1934,6 +1939,8 @@ def fetch_google_business_data(
('google_attributes', attributes if attributes else None),
('google_reviews_data', reviews_data if reviews_data else None),
('google_photos_metadata', photos_meta if photos_meta else None),
('google_maps_links', maps_links if maps_links else None),
('google_open_now', open_now),
]:
try:
setattr(analysis, attr, val)

View File

@ -33,7 +33,7 @@ PLACES_NEARBY_URL = "https://places.googleapis.com/v1/places:searchNearby"
BASIC_FIELDS = [
"id", "displayName", "formattedAddress", "location",
"types", "primaryType", "primaryTypeDisplayName",
"businessStatus", "googleMapsUri",
"businessStatus", "googleMapsUri", "googleMapsLinks",
"utcOffsetMinutes", "adrFormatAddress",
"shortFormattedAddress"
]

View File

@ -932,6 +932,24 @@
Zobacz wizytowke Google
</a>
{% endif %}
{% if places_data and places_data.maps_links %}
{% if places_data.maps_links.writeAReviewUri %}
<a href="{{ places_data.maps_links.writeAReviewUri }}" target="_blank" rel="noopener" class="btn btn-sm" style="background: #10b981; color: white; border: none;">
<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="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
Popros o opinie
</a>
{% endif %}
{% if places_data.maps_links.directionsUri %}
<a href="{{ places_data.maps_links.directionsUri }}" target="_blank" rel="noopener" class="btn btn-outline btn-sm">
<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 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
</svg>
Pokaz trase
</a>
{% endif %}
{% endif %}
{% if can_audit %}
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -953,6 +971,13 @@
<span class="score-label">/ 100</span>
</div>
<div class="score-details">
{% if places_data and places_data.open_now is not none %}
<div style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; margin-bottom: 8px; {% if places_data.open_now %}background: #d1fae5; color: #065f46;{% else %}background: #fee2e2; color: #991b1b;{% endif %}">
<span style="width: 8px; height: 8px; border-radius: 50%; {% if places_data.open_now %}background: #10b981;{% else %}background: #ef4444;{% endif %}"></span>
{% if places_data.open_now %}Otwarte{% else %}Zamkniete{% endif %}
<span style="font-weight: 400; font-size: 11px; opacity: 0.7;">(na moment audytu)</span>
</div>
{% endif %}
<div class="score-category" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
{% if score >= 90 %}
Doskonaly profil GBP
@ -1034,6 +1059,70 @@
</div>
{% endif %}
{% if places_data and places_data.google_name %}
<!-- NAP Comparison -->
<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" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
Porownanie NAP (Name, Address, Phone)
</h3>
<p style="font-size: 12px; color: var(--text-tertiary); margin: 0 0 var(--spacing-sm) 0;">Spojnosc danych NAP wplywa na lokalne SEO. Roznice moga obnizac widocznosc w Google.</p>
<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: 8px; color: var(--text-tertiary); font-weight: 500; width: 15%;">Pole</th>
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 35%;">Nasza baza</th>
<th style="text-align: left; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 35%;">Google</th>
<th style="text-align: center; padding: 8px; color: var(--text-tertiary); font-weight: 500; width: 15%;">Status</th>
</tr>
</thead>
<tbody>
{% set name_match = (company.name|lower|trim == places_data.google_name|lower|trim) if places_data.google_name else none %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; font-weight: 500;">Nazwa</td>
<td style="padding: 8px;">{{ company.name or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_name or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if name_match is none %}—
{% elif name_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
{% 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 %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px; font-weight: 500;">Adres</td>
<td style="padding: 8px;">{{ our_addr or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_address or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if addr_match is none %}—
{% elif addr_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
{% 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 %}
<tr>
<td style="padding: 8px; font-weight: 500;">Telefon</td>
<td style="padding: 8px;">{{ company.phone or '—' }}</td>
<td style="padding: 8px;">{{ places_data.google_phone or '—' }}</td>
<td style="padding: 8px; text-align: center;">
{% if phone_match is none %}—
{% elif phone_match %}<span style="color: #10b981; font-weight: 600;">&#10003;</span>
{% else %}<span style="color: #f97316; font-weight: 600;">&#10007;</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
<!-- Fields Section -->
<div class="fields-section">
<h2 class="section-title">