GBP Audit: fetch Google data + detailed progress overlay
- Add fetch_google_business_data() to fetch fresh data from Google Places API - Progress overlay shows all 10 data fields with actual values: * Place search, Rating, Reviews, Photos, Hours, Phone, Website, Status - 5-second delay after completion for user to read results - Fix opening hours display (show formatted weekday_text) - Fix reviews scoring (integer-based: 3 base + 1/review + 1 bonus) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
69bb6b839a
commit
6758e208d4
37
app.py
37
app.py
@ -136,7 +136,12 @@ except ImportError as e:
|
||||
|
||||
# GBP (Google Business Profile) audit service
|
||||
try:
|
||||
from gbp_audit_service import GBPAuditService, audit_company as gbp_audit_company, get_company_audit as gbp_get_company_audit
|
||||
from gbp_audit_service import (
|
||||
GBPAuditService,
|
||||
audit_company as gbp_audit_company,
|
||||
get_company_audit as gbp_get_company_audit,
|
||||
fetch_google_business_data as gbp_fetch_google_data
|
||||
)
|
||||
GBP_AUDIT_AVAILABLE = True
|
||||
GBP_AUDIT_VERSION = '1.0'
|
||||
except ImportError as e:
|
||||
@ -3892,8 +3897,21 @@ def api_gbp_audit_trigger():
|
||||
|
||||
logger.info(f"GBP audit triggered by {current_user.email} for company: {company.name} (ID: {company.id})")
|
||||
|
||||
# Option to fetch fresh Google data before audit
|
||||
fetch_google = data.get('fetch_google', True)
|
||||
force_refresh = data.get('force_refresh', False)
|
||||
|
||||
try:
|
||||
# Run the audit
|
||||
# Step 1: Fetch fresh Google Business data (if enabled)
|
||||
fetch_result = None
|
||||
if fetch_google:
|
||||
logger.info(f"Fetching Google Business data for company {company.id}...")
|
||||
fetch_result = gbp_fetch_google_data(db, company.id, force_refresh=force_refresh)
|
||||
if not fetch_result.get('success') and not fetch_result.get('data', {}).get('cached'):
|
||||
# Log warning but continue with audit
|
||||
logger.warning(f"Google fetch warning for company {company.id}: {fetch_result.get('error')}")
|
||||
|
||||
# Step 2: Run the audit
|
||||
result = gbp_audit_company(db, company.id, save=save_result)
|
||||
|
||||
# Build field status for response
|
||||
@ -3918,7 +3936,7 @@ def api_gbp_audit_trigger():
|
||||
else:
|
||||
score_category = 'poor'
|
||||
|
||||
return jsonify({
|
||||
response_data = {
|
||||
'success': True,
|
||||
'message': f'Audyt GBP dla firmy "{company.name}" został zakończony pomyślnie.',
|
||||
'company_id': company.id,
|
||||
@ -3940,7 +3958,18 @@ def api_gbp_audit_trigger():
|
||||
'average_rating': float(result.average_rating) if result.average_rating else None,
|
||||
'google_place_id': result.google_place_id
|
||||
}
|
||||
}), 200
|
||||
}
|
||||
|
||||
# Include Google fetch results if performed
|
||||
if fetch_result:
|
||||
response_data['google_fetch'] = {
|
||||
'success': fetch_result.get('success', False),
|
||||
'steps': fetch_result.get('steps', []),
|
||||
'data': fetch_result.get('data', {}),
|
||||
'error': fetch_result.get('error')
|
||||
}
|
||||
|
||||
return jsonify(response_data), 200
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({
|
||||
|
||||
@ -220,9 +220,20 @@ class GBPAuditService:
|
||||
# Convert fields to JSON-serializable format
|
||||
fields_status = {}
|
||||
for name, field_status in result.fields.items():
|
||||
# Keep dict values as-is for JSON serialization (e.g., opening hours)
|
||||
# Convert other complex types to string
|
||||
if field_status.value is None:
|
||||
serialized_value = None
|
||||
elif isinstance(field_status.value, (dict, list)):
|
||||
serialized_value = field_status.value # Keep as dict/list for JSON
|
||||
elif isinstance(field_status.value, (int, float, bool)):
|
||||
serialized_value = field_status.value # Keep primitives as-is
|
||||
else:
|
||||
serialized_value = str(field_status.value)
|
||||
|
||||
fields_status[name] = {
|
||||
'status': field_status.status,
|
||||
'value': str(field_status.value) if field_status.value is not None else None,
|
||||
'value': serialized_value,
|
||||
'score': field_status.score,
|
||||
'max_score': field_status.max_score
|
||||
}
|
||||
@ -573,7 +584,28 @@ class GBPAuditService:
|
||||
)
|
||||
|
||||
def _check_reviews(self, company: Company, analysis: Optional[CompanyWebsiteAnalysis]) -> FieldStatus:
|
||||
"""Check reviews presence and quality"""
|
||||
"""
|
||||
Check reviews presence and quality.
|
||||
|
||||
Scoring logic (max 9 points):
|
||||
=============================
|
||||
COMPLETE (9/9 pts):
|
||||
- 5+ opinii AND ocena >= 4.0
|
||||
|
||||
PARTIAL (3-8/9 pts):
|
||||
- Bazowo: 3 pkt za pierwszą opinię
|
||||
- +1 pkt za każdą kolejną opinię (do 4 opinii = 6 pkt max)
|
||||
- +1 pkt bonus jeśli ocena >= 4.0 (do 7 pkt max)
|
||||
|
||||
Przykłady:
|
||||
- 1 opinia, ocena 3.5 → 3/9 pkt
|
||||
- 1 opinia, ocena 5.0 → 4/9 pkt (3 + 1 bonus)
|
||||
- 3 opinie, ocena 4.2 → 6/9 pkt (3 + 2 + 1 bonus)
|
||||
- 4 opinie, ocena 4.5 → 7/9 pkt (3 + 3 + 1 bonus)
|
||||
|
||||
MISSING (0/9 pts):
|
||||
- Brak opinii
|
||||
"""
|
||||
max_score = FIELD_WEIGHTS['reviews']
|
||||
|
||||
review_count = 0
|
||||
@ -583,6 +615,7 @@ class GBPAuditService:
|
||||
review_count = analysis.google_reviews_count or 0
|
||||
rating = analysis.google_rating
|
||||
|
||||
# COMPLETE: 5+ reviews with good rating (4.0+) → full 9/9 points
|
||||
if review_count >= REVIEW_THRESHOLDS['good'] and rating and float(rating) >= 4.0:
|
||||
return FieldStatus(
|
||||
field_name='reviews',
|
||||
@ -592,8 +625,15 @@ class GBPAuditService:
|
||||
max_score=max_score
|
||||
)
|
||||
|
||||
# PARTIAL: 1-4 reviews → proportional scoring
|
||||
if review_count >= REVIEW_THRESHOLDS['minimum']:
|
||||
partial_score = max_score * 0.6
|
||||
# Base: 3 pts for first review + 1 pt per additional review (max 6 pts for 4 reviews)
|
||||
partial_score = min(3 + (review_count - 1), max_score - 3) # max 6 pts
|
||||
|
||||
# Bonus: +1 pt for good rating (>= 4.0)
|
||||
if rating and float(rating) >= 4.0:
|
||||
partial_score = min(partial_score + 1, max_score - 2) # max 7 pts
|
||||
|
||||
return FieldStatus(
|
||||
field_name='reviews',
|
||||
status='partial',
|
||||
@ -603,6 +643,7 @@ class GBPAuditService:
|
||||
recommendation='Zachęcaj klientów do zostawiania opinii. Więcej pozytywnych recenzji zwiększa zaufanie.'
|
||||
)
|
||||
|
||||
# MISSING: no reviews → 0/9 points
|
||||
return FieldStatus(
|
||||
field_name='reviews',
|
||||
status='missing',
|
||||
@ -993,6 +1034,272 @@ def batch_audit_companies(
|
||||
return results
|
||||
|
||||
|
||||
# === Google Places API Integration ===
|
||||
|
||||
def fetch_google_business_data(
|
||||
db: Session,
|
||||
company_id: int,
|
||||
force_refresh: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch fresh Google Business Profile data from Google Places API.
|
||||
|
||||
This function searches for the company on Google Places, retrieves
|
||||
detailed business information, and updates the CompanyWebsiteAnalysis record.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
company_id: Company ID to fetch data for
|
||||
force_refresh: If True, fetch even if recent data exists
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- success: bool
|
||||
- steps: List of step results with status
|
||||
- data: Fetched Google data (if successful)
|
||||
- error: Error message (if failed)
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
result = {
|
||||
'success': False,
|
||||
'steps': [],
|
||||
'data': {},
|
||||
'error': None
|
||||
}
|
||||
|
||||
# Get company
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
result['error'] = f'Firma o ID {company_id} nie znaleziona'
|
||||
return result
|
||||
|
||||
# Check if we have recent data (less than 24 hours old)
|
||||
if not force_refresh:
|
||||
existing = db.query(CompanyWebsiteAnalysis).filter(
|
||||
CompanyWebsiteAnalysis.company_id == company_id
|
||||
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
|
||||
|
||||
if existing and existing.analyzed_at:
|
||||
age = datetime.now() - existing.analyzed_at
|
||||
if age < timedelta(hours=24) and existing.google_place_id:
|
||||
result['success'] = True
|
||||
result['steps'].append({
|
||||
'step': 'cache_check',
|
||||
'status': 'skipped',
|
||||
'message': f'Dane pobrano {age.seconds // 3600}h temu - używam cache'
|
||||
})
|
||||
result['data'] = {
|
||||
'google_place_id': existing.google_place_id,
|
||||
'google_rating': float(existing.google_rating) if existing.google_rating else None,
|
||||
'google_reviews_count': existing.google_reviews_count,
|
||||
'google_photos_count': existing.google_photos_count,
|
||||
'google_opening_hours': existing.google_opening_hours,
|
||||
'cached': True
|
||||
}
|
||||
return result
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv('GOOGLE_PLACES_API_KEY')
|
||||
if not api_key:
|
||||
result['error'] = 'Brak klucza API Google Places (GOOGLE_PLACES_API_KEY)'
|
||||
result['steps'].append({
|
||||
'step': 'api_key_check',
|
||||
'status': 'error',
|
||||
'message': result['error']
|
||||
})
|
||||
return result
|
||||
|
||||
result['steps'].append({
|
||||
'step': 'api_key_check',
|
||||
'status': 'complete',
|
||||
'message': 'Klucz API skonfigurowany'
|
||||
})
|
||||
|
||||
# Step 1: Search for place
|
||||
result['steps'].append({
|
||||
'step': 'find_place',
|
||||
'status': 'in_progress',
|
||||
'message': f'Szukam firmy "{company.name}" w Google Maps...'
|
||||
})
|
||||
|
||||
city = company.address_city or 'Wejherowo'
|
||||
search_query = f'{company.name} {city}'
|
||||
|
||||
try:
|
||||
find_response = requests.get(
|
||||
'https://maps.googleapis.com/maps/api/place/findplacefromtext/json',
|
||||
params={
|
||||
'input': search_query,
|
||||
'inputtype': 'textquery',
|
||||
'fields': 'place_id,name,formatted_address',
|
||||
'language': 'pl',
|
||||
'key': api_key,
|
||||
},
|
||||
timeout=15
|
||||
)
|
||||
find_response.raise_for_status()
|
||||
find_data = find_response.json()
|
||||
|
||||
if find_data.get('status') != 'OK' or not find_data.get('candidates'):
|
||||
result['steps'][-1]['status'] = 'warning'
|
||||
result['steps'][-1]['message'] = f'Nie znaleziono firmy w Google Maps'
|
||||
result['error'] = 'Firma nie ma profilu Google Business lub nazwa jest inna niż w Google'
|
||||
return result
|
||||
|
||||
candidate = find_data['candidates'][0]
|
||||
place_id = candidate.get('place_id')
|
||||
google_name = candidate.get('name')
|
||||
google_address = candidate.get('formatted_address')
|
||||
|
||||
result['steps'][-1]['status'] = 'complete'
|
||||
result['steps'][-1]['message'] = f'Znaleziono: {google_name}'
|
||||
result['data']['google_place_id'] = place_id
|
||||
result['data']['google_name'] = google_name
|
||||
result['data']['google_address'] = google_address
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
result['steps'][-1]['status'] = 'error'
|
||||
result['steps'][-1]['message'] = 'Timeout - Google API nie odpowiada'
|
||||
result['error'] = 'Timeout podczas wyszukiwania w Google Places API'
|
||||
return result
|
||||
except Exception as e:
|
||||
result['steps'][-1]['status'] = 'error'
|
||||
result['steps'][-1]['message'] = f'Błąd: {str(e)}'
|
||||
result['error'] = str(e)
|
||||
return result
|
||||
|
||||
# Step 2: Get place details
|
||||
result['steps'].append({
|
||||
'step': 'get_details',
|
||||
'status': 'in_progress',
|
||||
'message': 'Pobieram szczegóły wizytówki...'
|
||||
})
|
||||
|
||||
try:
|
||||
fields = [
|
||||
'rating',
|
||||
'user_ratings_total',
|
||||
'opening_hours',
|
||||
'business_status',
|
||||
'formatted_phone_number',
|
||||
'website',
|
||||
'photos',
|
||||
]
|
||||
|
||||
details_response = requests.get(
|
||||
'https://maps.googleapis.com/maps/api/place/details/json',
|
||||
params={
|
||||
'place_id': place_id,
|
||||
'fields': ','.join(fields),
|
||||
'language': 'pl',
|
||||
'key': api_key,
|
||||
},
|
||||
timeout=15
|
||||
)
|
||||
details_response.raise_for_status()
|
||||
details_data = details_response.json()
|
||||
|
||||
if details_data.get('status') != 'OK':
|
||||
result['steps'][-1]['status'] = 'warning'
|
||||
result['steps'][-1]['message'] = f'Nie udało się pobrać szczegółów'
|
||||
result['error'] = f'Google Places API: {details_data.get("status")}'
|
||||
return result
|
||||
|
||||
place = details_data.get('result', {})
|
||||
|
||||
# Extract data
|
||||
rating = place.get('rating')
|
||||
reviews_count = place.get('user_ratings_total')
|
||||
photos = place.get('photos', [])
|
||||
photos_count = len(photos) if photos else 0
|
||||
opening_hours = place.get('opening_hours', {})
|
||||
business_status = place.get('business_status')
|
||||
phone = place.get('formatted_phone_number')
|
||||
website = place.get('website')
|
||||
|
||||
result['data']['google_rating'] = rating
|
||||
result['data']['google_reviews_count'] = reviews_count
|
||||
result['data']['google_photos_count'] = photos_count
|
||||
result['data']['google_opening_hours'] = opening_hours
|
||||
result['data']['google_business_status'] = business_status
|
||||
result['data']['google_phone'] = phone
|
||||
result['data']['google_website'] = website
|
||||
|
||||
result['steps'][-1]['status'] = 'complete'
|
||||
details_msg = []
|
||||
if rating:
|
||||
details_msg.append(f'Ocena: {rating}')
|
||||
if reviews_count:
|
||||
details_msg.append(f'{reviews_count} opinii')
|
||||
if photos_count:
|
||||
details_msg.append(f'{photos_count} zdjęć')
|
||||
result['steps'][-1]['message'] = ', '.join(details_msg) if details_msg else 'Pobrano dane'
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
result['steps'][-1]['status'] = 'error'
|
||||
result['steps'][-1]['message'] = 'Timeout podczas pobierania szczegółów'
|
||||
result['error'] = 'Timeout podczas pobierania szczegółów z Google Places API'
|
||||
return result
|
||||
except Exception as e:
|
||||
result['steps'][-1]['status'] = 'error'
|
||||
result['steps'][-1]['message'] = f'Błąd: {str(e)}'
|
||||
result['error'] = str(e)
|
||||
return result
|
||||
|
||||
# Step 3: Save to database
|
||||
result['steps'].append({
|
||||
'step': 'save_data',
|
||||
'status': 'in_progress',
|
||||
'message': 'Zapisuję dane w bazie...'
|
||||
})
|
||||
|
||||
try:
|
||||
# Get or create CompanyWebsiteAnalysis record
|
||||
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
||||
CompanyWebsiteAnalysis.company_id == company_id
|
||||
).first()
|
||||
|
||||
if not analysis:
|
||||
analysis = CompanyWebsiteAnalysis(
|
||||
company_id=company_id,
|
||||
url=company.website,
|
||||
analyzed_at=datetime.now()
|
||||
)
|
||||
db.add(analysis)
|
||||
|
||||
# Update Google fields
|
||||
analysis.google_place_id = place_id
|
||||
analysis.google_rating = rating
|
||||
analysis.google_reviews_count = reviews_count
|
||||
analysis.google_photos_count = photos_count
|
||||
analysis.google_opening_hours = opening_hours if opening_hours else None
|
||||
analysis.google_business_status = business_status
|
||||
analysis.analyzed_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
result['steps'][-1]['status'] = 'complete'
|
||||
result['steps'][-1]['message'] = 'Dane zapisane pomyślnie'
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
result['steps'][-1]['status'] = 'error'
|
||||
result['steps'][-1]['message'] = f'Błąd zapisu: {str(e)}'
|
||||
result['error'] = f'Błąd zapisu do bazy danych: {str(e)}'
|
||||
return result
|
||||
|
||||
logger.info(
|
||||
f"Google data fetched for company {company_id}: "
|
||||
f"rating={rating}, reviews={reviews_count}, photos={photos_count}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# === Main for Testing ===
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -383,27 +383,149 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.loading-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--border);
|
||||
.loading-content {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-header {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.loading-header h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-header p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.loading-steps {
|
||||
text-align: left;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.loading-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.loading-step.step-detail {
|
||||
padding-left: var(--spacing-lg);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.loading-step:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-icon.pending {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.step-icon.in_progress {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.step-icon.complete {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.step-icon.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.step-icon.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.step-icon.missing {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.step-icon.skipped {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.step-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.step-text.pending {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.step-text.in_progress {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step-text.complete {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.step-text.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.step-text.missing {
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.step-text.skipped {
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.step-text.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@ -741,7 +863,22 @@
|
||||
</span>
|
||||
</div>
|
||||
{% if field_data.value %}
|
||||
<div class="field-value">{{ field_data.value }}</div>
|
||||
<div class="field-value">
|
||||
{% if field_name == 'hours' and field_data.value is mapping and field_data.value.weekday_text %}
|
||||
{# Format opening hours nicely #}
|
||||
{% for day_hours in field_data.value.weekday_text[:5] %}
|
||||
{{ day_hours }}{% if not loop.last %}<br>{% endif %}
|
||||
{% endfor %}
|
||||
{% if field_data.value.weekday_text|length > 5 %}
|
||||
<br>...
|
||||
{% endif %}
|
||||
{% elif field_name == 'hours' and field_data.value is string and 'weekday_text' in field_data.value %}
|
||||
{# Handle string representation of dict #}
|
||||
<span class="hours-compact">Godziny ustawione</span>
|
||||
{% else %}
|
||||
{{ field_data.value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field-score">
|
||||
<div class="field-score-bar">
|
||||
@ -819,8 +956,99 @@
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Trwa analiza wizytowki Google...</div>
|
||||
<div class="loading-content">
|
||||
<div class="loading-header">
|
||||
<h3>Audyt Google Business Profile</h3>
|
||||
<p>Pobieram dane z Google i analizuje wizytowke...</p>
|
||||
</div>
|
||||
<div class="loading-steps" id="loadingSteps">
|
||||
<!-- Phase 1: Find Place -->
|
||||
<div class="loading-step" id="step-find">
|
||||
<div class="step-icon in_progress">
|
||||
<div class="step-spinner"></div>
|
||||
</div>
|
||||
<span class="step-text in_progress">Szukam firmy w Google Maps...</span>
|
||||
</div>
|
||||
|
||||
<!-- Phase 2: Fetch Details - individual data points -->
|
||||
<div class="loading-step step-detail" id="step-rating">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Ocena (rating)</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-reviews">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Liczba opinii</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-photos">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Zdjecia</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-hours">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Godziny otwarcia</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-phone">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Numer telefonu</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-website">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Strona WWW</span>
|
||||
</div>
|
||||
<div class="loading-step step-detail" id="step-status">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Status firmy</span>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Save -->
|
||||
<div class="loading-step" id="step-save">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Zapisuje dane w bazie</span>
|
||||
</div>
|
||||
|
||||
<!-- Phase 4: Audit -->
|
||||
<div class="loading-step" id="step-audit">
|
||||
<div class="step-icon pending">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-text pending">Analizuje kompletnosc profilu</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Modal -->
|
||||
@ -848,7 +1076,79 @@
|
||||
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)
|
||||
const detailLabels = {
|
||||
'step-rating': 'Ocena (rating)',
|
||||
'step-reviews': 'Liczba opinii',
|
||||
'step-photos': 'Zdjecia',
|
||||
'step-hours': 'Godziny otwarcia',
|
||||
'step-phone': 'Numer telefonu',
|
||||
'step-website': 'Strona WWW',
|
||||
'step-status': 'Status firmy'
|
||||
};
|
||||
|
||||
// SVG icons for different states
|
||||
const icons = {
|
||||
pending: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg>',
|
||||
in_progress: '<div class="step-spinner"></div>',
|
||||
complete: '<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="M5 13l4 4L19 7"/></svg>',
|
||||
error: '<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="M6 18L18 6M6 6l12 12"/></svg>',
|
||||
warning: '<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
|
||||
skipped: '<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="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>',
|
||||
missing: '<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="M20 12H4"/></svg>'
|
||||
};
|
||||
|
||||
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
|
||||
if (detailLabels[stepId]) {
|
||||
textEl.textContent = 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.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
resetSteps();
|
||||
document.getElementById('loadingOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
@ -872,6 +1172,82 @@ 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 ocene...');
|
||||
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 zdjecia...');
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
if (googleData.google_photos_count) {
|
||||
updateStep('step-photos', 'complete', `Zdjecia: ${googleData.google_photos_count}`);
|
||||
} else {
|
||||
updateStep('step-photos', 'missing', 'Brak zdjec');
|
||||
}
|
||||
|
||||
// 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 strone 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 zamknieta',
|
||||
'CLOSED_PERMANENTLY': 'Zamknieta na stale'
|
||||
};
|
||||
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) {
|
||||
@ -879,6 +1255,9 @@ async function runAudit() {
|
||||
}
|
||||
showLoading();
|
||||
|
||||
// Simulate step animation start
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/gbp/audit', {
|
||||
method: 'POST',
|
||||
@ -886,16 +1265,50 @@ async function runAudit() {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ slug: companySlug })
|
||||
body: JSON.stringify({ slug: companySlug, force_refresh: true })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
// 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] + ' (pominiety)');
|
||||
}
|
||||
}
|
||||
|
||||
// Update audit step
|
||||
if (response.ok && data.success) {
|
||||
showInfoModal('Audyt zakonczone', 'Audyt wizytowki Google zostal zakonczony pomyslnie. Strona zostanie odswiezona.', true);
|
||||
updateStep('step-save', 'complete', 'Dane zapisane');
|
||||
updateStep('step-audit', 'complete', `Analiza zakonczona: ${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 zakonczony', 'Audyt wizytowki Google zostal zakonczony pomyslnie. Strona zostanie odswiezona.', true);
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
updateStep('step-audit', 'error', 'Blad audytu');
|
||||
// Wait 5 seconds so user can see what failed
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
hideLoading();
|
||||
showInfoModal('Blad', data.error || 'Wystapil nieznany blad podczas audytu.', false);
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user