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
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:
parent
137f45d0c6
commit
ca051be435
@ -349,6 +349,11 @@ def gbp_audit_dashboard(slug):
|
|||||||
'primary_type': getattr(analysis, 'google_primary_type', None),
|
'primary_type': getattr(analysis, 'google_primary_type', None),
|
||||||
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
|
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
|
||||||
'price_level': getattr(analysis, 'google_price_level', 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)
|
# If no audit exists, we still render the page (template handles this)
|
||||||
|
|||||||
@ -1154,6 +1154,8 @@ class CompanyWebsiteAnalysis(Base):
|
|||||||
google_attributes = Column(JSONB) # Places API: business attributes
|
google_attributes = Column(JSONB) # Places API: business attributes
|
||||||
google_reviews_data = Column(JSONB) # Places API: reviews with text/rating
|
google_reviews_data = Column(JSONB) # Places API: reviews with text/rating
|
||||||
google_photos_metadata = Column(JSONB) # Places API: photo references
|
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 METADATA ===
|
||||||
seo_audit_version = Column(String(20)) # Version of SEO audit script used
|
seo_audit_version = Column(String(20)) # Version of SEO audit script used
|
||||||
|
|||||||
5
database/migrations/062_google_maps_links.sql
Normal file
5
database/migrations/062_google_maps_links.sql
Normal 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;
|
||||||
@ -1837,6 +1837,9 @@ def fetch_google_business_data(
|
|||||||
business_status = place_data.get('businessStatus', '')
|
business_status = place_data.get('businessStatus', '')
|
||||||
editorial_summary = place_data.get('editorialSummary', {}).get('text', '')
|
editorial_summary = place_data.get('editorialSummary', {}).get('text', '')
|
||||||
price_level = place_data.get('priceLevel', '')
|
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
|
# Extract rich data using service methods
|
||||||
reviews_data = places_service.extract_reviews_data(place_data)
|
reviews_data = places_service.extract_reviews_data(place_data)
|
||||||
@ -1876,6 +1879,8 @@ def fetch_google_business_data(
|
|||||||
'google_reviews_data': reviews_data,
|
'google_reviews_data': reviews_data,
|
||||||
'google_photos_metadata': photos_meta,
|
'google_photos_metadata': photos_meta,
|
||||||
'google_has_special_hours': hours_data.get('has_special_hours', False),
|
'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'
|
result['steps'][-1]['status'] = 'complete'
|
||||||
@ -1934,6 +1939,8 @@ def fetch_google_business_data(
|
|||||||
('google_attributes', attributes if attributes else None),
|
('google_attributes', attributes if attributes else None),
|
||||||
('google_reviews_data', reviews_data if reviews_data else None),
|
('google_reviews_data', reviews_data if reviews_data else None),
|
||||||
('google_photos_metadata', photos_meta if photos_meta 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:
|
try:
|
||||||
setattr(analysis, attr, val)
|
setattr(analysis, attr, val)
|
||||||
|
|||||||
@ -33,7 +33,7 @@ PLACES_NEARBY_URL = "https://places.googleapis.com/v1/places:searchNearby"
|
|||||||
BASIC_FIELDS = [
|
BASIC_FIELDS = [
|
||||||
"id", "displayName", "formattedAddress", "location",
|
"id", "displayName", "formattedAddress", "location",
|
||||||
"types", "primaryType", "primaryTypeDisplayName",
|
"types", "primaryType", "primaryTypeDisplayName",
|
||||||
"businessStatus", "googleMapsUri",
|
"businessStatus", "googleMapsUri", "googleMapsLinks",
|
||||||
"utcOffsetMinutes", "adrFormatAddress",
|
"utcOffsetMinutes", "adrFormatAddress",
|
||||||
"shortFormattedAddress"
|
"shortFormattedAddress"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -932,6 +932,24 @@
|
|||||||
Zobacz wizytowke Google
|
Zobacz wizytowke Google
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if can_audit %}
|
||||||
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
|
<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">
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -953,6 +971,13 @@
|
|||||||
<span class="score-label">/ 100</span>
|
<span class="score-label">/ 100</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-details">
|
<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 %};">
|
<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 %}
|
{% if score >= 90 %}
|
||||||
Doskonaly profil GBP
|
Doskonaly profil GBP
|
||||||
@ -1034,6 +1059,70 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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;">✓</span>
|
||||||
|
{% else %}<span style="color: #f97316; font-weight: 600;">✗</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;">✓</span>
|
||||||
|
{% else %}<span style="color: #f97316; font-weight: 600;">✗</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;">✓</span>
|
||||||
|
{% else %}<span style="color: #f97316; font-weight: 600;">✗</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Fields Section -->
|
<!-- Fields Section -->
|
||||||
<div class="fields-section">
|
<div class="fields-section">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user