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
After token exchange, fetches Google userinfo to save the email and name of the Google account used for authorization. Displays this info on the GBP audit page so users know which account to reconnect with. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
655 lines
31 KiB
Python
655 lines
31 KiB
Python
"""
|
|
Audit Dashboard Routes - Audit blueprint
|
|
|
|
Migrated from app.py as part of the blueprint refactoring.
|
|
Contains user-facing audit dashboard pages for SEO, GBP, Social Media, and IT.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from flask import abort, flash, redirect, render_template, url_for
|
|
from flask_login import current_user, login_required
|
|
from utils.decorators import is_audit_owner
|
|
|
|
from database import (
|
|
SessionLocal, Company, CompanyWebsiteAnalysis,
|
|
CompanySocialMedia, ITAudit, CompanyCitation, GBPReview, OAuthToken
|
|
)
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Check if GBP audit service is available
|
|
try:
|
|
from gbp_audit_service import (
|
|
get_company_audit as gbp_get_company_audit
|
|
)
|
|
GBP_AUDIT_AVAILABLE = True
|
|
GBP_AUDIT_VERSION = '1.0'
|
|
except ImportError:
|
|
GBP_AUDIT_AVAILABLE = False
|
|
GBP_AUDIT_VERSION = None
|
|
gbp_get_company_audit = None
|
|
|
|
|
|
# ============================================================
|
|
# SEO AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/seo/<slug>')
|
|
@login_required
|
|
def seo_audit_dashboard(slug):
|
|
"""
|
|
User-facing SEO audit dashboard for a specific company.
|
|
|
|
Displays SEO audit results with:
|
|
- PageSpeed Insights scores (SEO, Performance, Accessibility, Best Practices)
|
|
- Website analysis data
|
|
- Improvement recommendations
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered seo_audit.html template with company and audit data
|
|
"""
|
|
if not is_audit_owner():
|
|
abort(404)
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest SEO analysis for this company
|
|
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
|
CompanyWebsiteAnalysis.company_id == company.id
|
|
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).first()
|
|
|
|
# Get citations for this company
|
|
citations = db.query(CompanyCitation).filter(
|
|
CompanyCitation.company_id == company.id
|
|
).order_by(CompanyCitation.directory_name).all() if analysis else []
|
|
|
|
# Build SEO data dict if analysis exists
|
|
seo_data = None
|
|
if analysis and analysis.seo_audited_at:
|
|
seo_data = {
|
|
'seo_score': analysis.pagespeed_seo_score,
|
|
'performance_score': analysis.pagespeed_performance_score,
|
|
'accessibility_score': analysis.pagespeed_accessibility_score,
|
|
'best_practices_score': analysis.pagespeed_best_practices_score,
|
|
'audited_at': analysis.seo_audited_at,
|
|
'audit_version': analysis.seo_audit_version,
|
|
'url': analysis.website_url,
|
|
# Local SEO fields
|
|
'local_seo_score': analysis.local_seo_score,
|
|
'has_local_business_schema': analysis.has_local_business_schema,
|
|
'local_business_schema_fields': analysis.local_business_schema_fields,
|
|
'nap_on_website': analysis.nap_on_website,
|
|
'has_google_maps_embed': analysis.has_google_maps_embed,
|
|
'has_local_keywords': analysis.has_local_keywords,
|
|
'local_keywords_found': analysis.local_keywords_found,
|
|
'citations_count': analysis.citations_count,
|
|
'content_freshness_score': analysis.content_freshness_score,
|
|
'last_modified_date': analysis.last_modified_at,
|
|
# Core Web Vitals
|
|
'lcp_ms': analysis.largest_contentful_paint_ms,
|
|
'inp_ms': analysis.interaction_to_next_paint_ms,
|
|
'cls': float(analysis.cumulative_layout_shift) if analysis.cumulative_layout_shift is not None else None,
|
|
# Heading structure
|
|
'h1_count': analysis.h1_count,
|
|
'h1_text': analysis.h1_text,
|
|
'h2_count': analysis.h2_count,
|
|
'h3_count': analysis.h3_count,
|
|
# Image SEO
|
|
'total_images': analysis.total_images,
|
|
'images_without_alt': analysis.images_without_alt,
|
|
# Links
|
|
'internal_links_count': analysis.internal_links_count,
|
|
'external_links_count': analysis.external_links_count,
|
|
'broken_links_count': analysis.broken_links_count,
|
|
# SSL
|
|
'has_ssl': analysis.has_ssl,
|
|
'ssl_expires_at': analysis.ssl_expires_at,
|
|
# Analytics
|
|
'has_google_analytics': analysis.has_google_analytics,
|
|
'has_google_tag_manager': analysis.has_google_tag_manager,
|
|
# Social sharing
|
|
'has_og_tags': analysis.has_og_tags,
|
|
'has_twitter_cards': analysis.has_twitter_cards,
|
|
# Technical SEO details
|
|
'has_sitemap': analysis.has_sitemap,
|
|
'has_robots_txt': analysis.has_robots_txt,
|
|
'has_canonical': analysis.has_canonical,
|
|
'canonical_url': analysis.canonical_url,
|
|
'is_indexable': analysis.is_indexable,
|
|
'noindex_reason': analysis.noindex_reason,
|
|
'is_mobile_friendly': analysis.is_mobile_friendly,
|
|
'viewport_configured': analysis.viewport_configured,
|
|
'html_lang': analysis.html_lang,
|
|
'has_hreflang': analysis.has_hreflang,
|
|
# Meta tags
|
|
'meta_title': analysis.meta_title,
|
|
'meta_description': analysis.meta_description,
|
|
# Structured data
|
|
'has_structured_data': analysis.has_structured_data,
|
|
'structured_data_types': analysis.structured_data_types,
|
|
# Performance
|
|
'load_time_ms': analysis.load_time_ms,
|
|
'word_count_homepage': analysis.word_count_homepage,
|
|
# CrUX field data (real user metrics)
|
|
'crux_lcp_ms': analysis.crux_lcp_ms,
|
|
'crux_inp_ms': analysis.crux_inp_ms,
|
|
'crux_cls': float(analysis.crux_cls) if analysis.crux_cls is not None else None,
|
|
'crux_fcp_ms': analysis.crux_fcp_ms,
|
|
'crux_ttfb_ms': analysis.crux_ttfb_ms,
|
|
'crux_lcp_good_pct': float(analysis.crux_lcp_good_pct) if analysis.crux_lcp_good_pct is not None else None,
|
|
'crux_inp_good_pct': float(analysis.crux_inp_good_pct) if analysis.crux_inp_good_pct is not None else None,
|
|
# Security headers
|
|
'has_hsts': analysis.has_hsts,
|
|
'has_csp': analysis.has_csp,
|
|
'has_x_frame_options': analysis.has_x_frame_options,
|
|
'has_x_content_type_options': analysis.has_x_content_type_options,
|
|
'security_headers_count': analysis.security_headers_count,
|
|
# Image formats
|
|
'modern_image_count': analysis.modern_image_count,
|
|
'legacy_image_count': analysis.legacy_image_count,
|
|
'modern_image_ratio': float(analysis.modern_image_ratio) if analysis.modern_image_ratio is not None else None,
|
|
# Google Search Console (OAuth)
|
|
'gsc_clicks': analysis.gsc_clicks,
|
|
'gsc_impressions': analysis.gsc_impressions,
|
|
'gsc_ctr': float(analysis.gsc_ctr) if analysis.gsc_ctr is not None else None,
|
|
'gsc_avg_position': float(analysis.gsc_avg_position) if analysis.gsc_avg_position is not None else None,
|
|
'gsc_top_queries': analysis.gsc_top_queries,
|
|
'gsc_period_days': analysis.gsc_period_days,
|
|
# GSC Extended data
|
|
'gsc_top_pages': getattr(analysis, 'gsc_top_pages', None),
|
|
'gsc_device_breakdown': getattr(analysis, 'gsc_device_breakdown', None),
|
|
'gsc_index_status': getattr(analysis, 'gsc_index_status', None),
|
|
'gsc_last_crawl': getattr(analysis, 'gsc_last_crawl', None),
|
|
'gsc_crawled_as': getattr(analysis, 'gsc_crawled_as', None),
|
|
'gsc_sitemaps': getattr(analysis, 'gsc_sitemaps', None),
|
|
'gsc_country_breakdown': getattr(analysis, 'gsc_country_breakdown', None),
|
|
'gsc_search_type_breakdown': getattr(analysis, 'gsc_search_type_breakdown', None),
|
|
'gsc_trend_data': getattr(analysis, 'gsc_trend_data', None),
|
|
# Citations list
|
|
'citations': [{'directory_name': c.directory_name, 'listing_url': c.listing_url, 'status': c.status, 'nap_accurate': c.nap_accurate} for c in citations],
|
|
}
|
|
|
|
# Check if company has GSC OAuth token
|
|
has_gsc_token = db.query(OAuthToken).filter(
|
|
OAuthToken.company_id == company.id,
|
|
OAuthToken.provider == 'google',
|
|
OAuthToken.service == 'search_console'
|
|
).first() is not None
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('seo_audit.html',
|
|
company=company,
|
|
seo_data=seo_data,
|
|
can_audit=can_audit,
|
|
has_gsc_token=has_gsc_token
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# SOCIAL MEDIA AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/social/<slug>')
|
|
@login_required
|
|
def social_audit_dashboard(slug):
|
|
"""
|
|
User-facing Social Media audit dashboard for a specific company.
|
|
|
|
Displays social media presence audit with:
|
|
- Overall presence score (platforms found / total platforms)
|
|
- Platform-by-platform status
|
|
- Profile validation status
|
|
- Recommendations for missing platforms
|
|
|
|
Access control:
|
|
- Admins: Can view all companies
|
|
- Regular users: Can only view their own company
|
|
|
|
Args:
|
|
slug: Company URL slug
|
|
|
|
Returns:
|
|
Rendered social_audit.html template with company and social data
|
|
"""
|
|
if not is_audit_owner():
|
|
abort(404)
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control - users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get social media profiles for this company
|
|
social_profiles = db.query(CompanySocialMedia).filter(
|
|
CompanySocialMedia.company_id == company.id
|
|
).all()
|
|
|
|
# Define all platforms we track
|
|
all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok']
|
|
|
|
# Build social media data
|
|
profiles_dict = {}
|
|
for profile in social_profiles:
|
|
profiles_dict[profile.platform] = {
|
|
'url': profile.url,
|
|
'is_valid': profile.is_valid,
|
|
'check_status': profile.check_status,
|
|
'page_name': profile.page_name,
|
|
'followers_count': profile.followers_count,
|
|
'verified_at': profile.verified_at,
|
|
'last_checked_at': profile.last_checked_at,
|
|
# Enhanced audit fields
|
|
'has_profile_photo': profile.has_profile_photo,
|
|
'has_cover_photo': profile.has_cover_photo,
|
|
'has_bio': profile.has_bio,
|
|
'profile_description': profile.profile_description,
|
|
'posts_count_30d': profile.posts_count_30d,
|
|
'posts_count_365d': profile.posts_count_365d,
|
|
'last_post_date': profile.last_post_date,
|
|
'posting_frequency_score': profile.posting_frequency_score,
|
|
'engagement_rate': profile.engagement_rate,
|
|
'content_types': profile.content_types,
|
|
'profile_completeness_score': profile.profile_completeness_score,
|
|
'followers_history': profile.followers_history,
|
|
'source': profile.source,
|
|
}
|
|
|
|
# Calculate score (platforms with profiles / total platforms)
|
|
platforms_with_profiles = len([p for p in all_platforms if p in profiles_dict])
|
|
total_platforms = len(all_platforms)
|
|
score = int((platforms_with_profiles / total_platforms) * 100) if total_platforms > 0 else 0
|
|
|
|
social_data = {
|
|
'profiles': profiles_dict,
|
|
'all_platforms': all_platforms,
|
|
'platforms_count': platforms_with_profiles,
|
|
'total_platforms': total_platforms,
|
|
'score': score
|
|
}
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('social_audit.html',
|
|
company=company,
|
|
social_data=social_data,
|
|
can_audit=can_audit
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# GBP AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/gbp/<slug>')
|
|
@login_required
|
|
def gbp_audit_dashboard(slug):
|
|
"""
|
|
User-facing GBP audit dashboard for a specific company.
|
|
|
|
Displays Google Business Profile completeness audit results with:
|
|
- Overall completeness score (0-100)
|
|
- Field-by-field status breakdown
|
|
- AI-generated improvement recommendations
|
|
- Historical audit data
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered gbp_audit.html template with company and audit data
|
|
"""
|
|
if not is_audit_owner():
|
|
abort(404)
|
|
if not GBP_AUDIT_AVAILABLE:
|
|
flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest audit for this company
|
|
audit = gbp_get_company_audit(db, company.id)
|
|
|
|
# Get recent reviews for this company
|
|
recent_reviews = db.query(GBPReview).filter(
|
|
GBPReview.company_id == company.id
|
|
).order_by(GBPReview.publish_time.desc()).limit(5).all() if audit else []
|
|
|
|
# Get Places API enrichment data from CompanyWebsiteAnalysis
|
|
places_data = {}
|
|
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
|
CompanyWebsiteAnalysis.company_id == company.id
|
|
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
|
|
# Polish translations for common Google Places types
|
|
_type_pl = {
|
|
'accounting': 'Biuro rachunkowe', 'airport': 'Lotnisko', 'atm': 'Bankomat',
|
|
'bakery': 'Piekarnia', 'bank': 'Bank', 'bar': 'Bar', 'beauty_salon': 'Salon kosmetyczny',
|
|
'book_store': 'Księgarnia', 'cafe': 'Kawiarnia', 'car_dealer': 'Dealer samochodowy',
|
|
'car_repair': 'Warsztat samochodowy', 'car_wash': 'Myjnia samochodowa',
|
|
'clothing_store': 'Sklep odzieżowy', 'construction_company': 'Firma budowlana',
|
|
'convenience_store': 'Sklep spożywczy', 'dentist': 'Dentysta', 'doctor': 'Lekarz',
|
|
'electrician': 'Elektryk', 'electronics_store': 'Sklep elektroniczny',
|
|
'establishment': 'Firma', 'finance': 'Finanse', 'florist': 'Kwiaciarnia',
|
|
'food': 'Gastronomia', 'furniture_store': 'Sklep meblowy', 'gas_station': 'Stacja benzynowa',
|
|
'general_contractor': 'Generalny wykonawca', 'grocery_or_supermarket': 'Sklep/supermarket',
|
|
'gym': 'Siłownia', 'hair_care': 'Fryzjer', 'hardware_store': 'Sklep budowlany',
|
|
'health': 'Zdrowie', 'home_goods_store': 'Sklep z wyposażeniem domu',
|
|
'insurance_agency': 'Agencja ubezpieczeniowa', 'lawyer': 'Prawnik',
|
|
'locksmith': 'Ślusarz', 'lodging': 'Nocleg', 'meal_delivery': 'Dostawa jedzenia',
|
|
'meal_takeaway': 'Jedzenie na wynos', 'moving_company': 'Firma przeprowadzkowa',
|
|
'painter': 'Malarz', 'parking': 'Parking', 'pet_store': 'Sklep zoologiczny',
|
|
'pharmacy': 'Apteka', 'physiotherapist': 'Fizjoterapeuta', 'plumber': 'Hydraulik',
|
|
'point_of_interest': 'Punkt zainteresowania', 'real_estate_agency': 'Agencja nieruchomości',
|
|
'restaurant': 'Restauracja', 'roofing_contractor': 'Dekarz',
|
|
'school': 'Szkoła', 'shoe_store': 'Sklep obuwniczy', 'shopping_mall': 'Centrum handlowe',
|
|
'spa': 'Spa', 'store': 'Sklep', 'supermarket': 'Supermarket',
|
|
'travel_agency': 'Biuro podróży', 'veterinary_care': 'Weterynarz',
|
|
'computer_store': 'Sklep komputerowy', 'it_services': 'Usługi IT',
|
|
'corporate_office': 'Biuro', 'consultant': 'Konsultant',
|
|
'marketing_agency': 'Agencja marketingowa', 'photographer': 'Fotograf',
|
|
'printing_service': 'Drukarnia', 'sign_shop': 'Reklama wizualna',
|
|
'auto_body_shop': 'Blacharnia', 'auto_parts_store': 'Sklep z częściami',
|
|
}
|
|
|
|
if analysis:
|
|
_ptype = getattr(analysis, 'google_primary_type', None) or ''
|
|
places_data = {
|
|
'primary_type': _ptype,
|
|
'primary_type_display': _type_pl.get(_ptype, _ptype.replace('_', ' ').title()) if _ptype else 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,
|
|
# GBP Performance API data
|
|
'gbp_impressions_maps': getattr(analysis, 'gbp_impressions_maps', None),
|
|
'gbp_impressions_search': getattr(analysis, 'gbp_impressions_search', None),
|
|
'gbp_call_clicks': getattr(analysis, 'gbp_call_clicks', None),
|
|
'gbp_website_clicks': getattr(analysis, 'gbp_website_clicks', None),
|
|
'gbp_direction_requests': getattr(analysis, 'gbp_direction_requests', None),
|
|
'gbp_conversations': getattr(analysis, 'gbp_conversations', None),
|
|
'gbp_search_keywords': getattr(analysis, 'gbp_search_keywords', None),
|
|
'gbp_performance_period_days': getattr(analysis, 'gbp_performance_period_days', None),
|
|
# Owner data
|
|
'google_owner_responses_count': getattr(analysis, 'google_owner_responses_count', None),
|
|
'google_review_response_rate': float(analysis.google_review_response_rate) if getattr(analysis, 'google_review_response_rate', None) is not None else None,
|
|
'google_posts_data': getattr(analysis, 'google_posts_data', None),
|
|
'google_posts_count': getattr(analysis, 'google_posts_count', None),
|
|
# Additional data not previously exposed
|
|
'google_business_status': analysis.google_business_status,
|
|
'google_opening_hours': analysis.google_opening_hours,
|
|
'google_photos_count': analysis.google_photos_count,
|
|
'google_website': analysis.google_website,
|
|
'google_types': analysis.google_types,
|
|
'google_maps_url': analysis.google_maps_url,
|
|
'google_rating': float(analysis.google_rating) if analysis.google_rating is not None else None,
|
|
'google_reviews_count': analysis.google_reviews_count,
|
|
'google_attributes': getattr(analysis, 'google_attributes', None),
|
|
'google_reviews_data': getattr(analysis, 'google_reviews_data', None),
|
|
'google_photos_metadata': getattr(analysis, 'google_photos_metadata', None),
|
|
'google_place_id': analysis.google_place_id,
|
|
'has_google_analytics': getattr(analysis, 'has_google_analytics', None),
|
|
'has_google_tag_manager': getattr(analysis, 'has_google_tag_manager', None),
|
|
'has_google_maps_embed': getattr(analysis, 'has_google_maps_embed', None),
|
|
}
|
|
|
|
# Build GBP recommendations
|
|
gbp_recommendations = []
|
|
if analysis.google_name:
|
|
if not analysis.google_opening_hours:
|
|
gbp_recommendations.append({'severity': 'warning', 'text': 'Brak godzin otwarcia w Google. Klienci nie wiedza kiedy firma jest czynna.'})
|
|
if not analysis.google_phone:
|
|
gbp_recommendations.append({'severity': 'critical', 'text': 'Brak numeru telefonu w Google. To podstawowy sposob kontaktu klientow.'})
|
|
if (analysis.google_photos_count or 0) < 5:
|
|
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 zdjec w profilu Google. Dodaj zdjecia — firmy ze zdjeciami otrzymuja 42%% wiecej zapytan o dojazd.'})
|
|
if (analysis.google_reviews_count or 0) < 5:
|
|
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 opinii Google. Zachecaj klientow do wystawienia recenzji.'})
|
|
rr = float(analysis.google_review_response_rate) if analysis.google_review_response_rate is not None else None
|
|
if rr is not None and rr < 50:
|
|
gbp_recommendations.append({'severity': 'warning', 'text': 'Odpowiedzi na mniej niz 50%% opinii. Odpowiadanie na recenzje poprawia widocznosc.'})
|
|
if not analysis.google_website:
|
|
gbp_recommendations.append({'severity': 'info', 'text': 'Brak strony WWW w profilu Google. Dodaj link do strony firmy.'})
|
|
if analysis.google_business_status and analysis.google_business_status != 'OPERATIONAL':
|
|
gbp_recommendations.append({'severity': 'critical', 'text': 'Profil Google nie ma statusu OPERATIONAL. Sprawdz czy firma jest oznaczona jako czynna.'})
|
|
if not gbp_recommendations:
|
|
gbp_recommendations.append({'severity': 'success', 'text': 'Profil Google wyglada dobrze! Nie znaleziono powaznych problemow.'})
|
|
|
|
# GBP benchmarks — average across all members
|
|
gbp_benchmarks = {}
|
|
try:
|
|
from sqlalchemy import func as sqlfunc
|
|
bench = db.query(
|
|
sqlfunc.avg(CompanyWebsiteAnalysis.google_rating),
|
|
sqlfunc.avg(CompanyWebsiteAnalysis.google_reviews_count),
|
|
sqlfunc.avg(CompanyWebsiteAnalysis.google_photos_count),
|
|
sqlfunc.count(CompanyWebsiteAnalysis.id),
|
|
).join(Company, Company.id == CompanyWebsiteAnalysis.company_id
|
|
).filter(
|
|
Company.status == 'active',
|
|
CompanyWebsiteAnalysis.google_rating.isnot(None),
|
|
).first()
|
|
if bench and bench[3] > 1:
|
|
gbp_benchmarks = {
|
|
'count': bench[3],
|
|
'avg_rating': round(float(bench[0] or 0), 1),
|
|
'avg_reviews': round(float(bench[1] or 0)),
|
|
'avg_photos': round(float(bench[2] or 0)),
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
# If no audit exists, we still render the page (template handles this)
|
|
# The user can trigger an audit from the dashboard
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
# Check GBP OAuth connection status
|
|
gbp_oauth_token = db.query(OAuthToken).filter(
|
|
OAuthToken.company_id == company.id,
|
|
OAuthToken.provider == 'google',
|
|
OAuthToken.service == 'gbp'
|
|
).first()
|
|
gbp_connection = None
|
|
if gbp_oauth_token:
|
|
from datetime import datetime
|
|
is_expired = gbp_oauth_token.expires_at and gbp_oauth_token.expires_at < datetime.utcnow()
|
|
has_location = bool(gbp_oauth_token.metadata_json and isinstance(gbp_oauth_token.metadata_json, dict) and gbp_oauth_token.metadata_json.get('location_name'))
|
|
gbp_connection = {
|
|
'connected': True,
|
|
'is_active': gbp_oauth_token.is_active,
|
|
'is_expired': is_expired,
|
|
'has_location': has_location,
|
|
'created_at': gbp_oauth_token.created_at,
|
|
'expires_at': gbp_oauth_token.expires_at,
|
|
'has_refresh_token': bool(gbp_oauth_token.refresh_token),
|
|
'google_email': gbp_oauth_token.account_id,
|
|
'google_name': gbp_oauth_token.account_name,
|
|
}
|
|
|
|
logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('gbp_audit.html',
|
|
company=company,
|
|
audit=audit,
|
|
recent_reviews=recent_reviews,
|
|
places_data=places_data,
|
|
can_audit=can_audit,
|
|
gbp_audit_available=GBP_AUDIT_AVAILABLE,
|
|
gbp_audit_version=GBP_AUDIT_VERSION,
|
|
gbp_recommendations=gbp_recommendations if analysis else [],
|
|
gbp_benchmarks=gbp_benchmarks if analysis else {},
|
|
gbp_connection=gbp_connection
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# IT AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/it/<slug>')
|
|
@login_required
|
|
def it_audit_dashboard(slug):
|
|
"""
|
|
User-facing IT infrastructure audit dashboard for a specific company.
|
|
|
|
Displays IT audit results with:
|
|
- Overall score and maturity level
|
|
- Security, collaboration, and completeness sub-scores
|
|
- Technology stack summary (Azure AD, M365, backup, monitoring)
|
|
- AI-generated recommendations
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered it_audit.html template with company and audit data
|
|
"""
|
|
if not is_audit_owner():
|
|
abort(404)
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest IT audit for this company
|
|
audit = db.query(ITAudit).filter(
|
|
ITAudit.company_id == company.id
|
|
).order_by(ITAudit.audit_date.desc()).first()
|
|
|
|
# Build audit data dict if audit exists
|
|
audit_data = None
|
|
if audit:
|
|
# Get maturity label
|
|
maturity_labels = {
|
|
'basic': 'Podstawowy',
|
|
'developing': 'Rozwijający się',
|
|
'established': 'Ugruntowany',
|
|
'advanced': 'Zaawansowany'
|
|
}
|
|
|
|
audit_data = {
|
|
'id': audit.id,
|
|
'overall_score': audit.overall_score,
|
|
'security_score': audit.security_score,
|
|
'collaboration_score': audit.collaboration_score,
|
|
'completeness_score': audit.completeness_score,
|
|
'maturity_level': audit.maturity_level,
|
|
'maturity_label': maturity_labels.get(audit.maturity_level, 'Nieznany'),
|
|
'audit_date': audit.audit_date,
|
|
'audit_source': audit.audit_source,
|
|
# Technology flags
|
|
'has_azure_ad': audit.has_azure_ad,
|
|
'has_m365': audit.has_m365,
|
|
'has_google_workspace': audit.has_google_workspace,
|
|
'has_local_ad': audit.has_local_ad,
|
|
'has_edr': audit.has_edr,
|
|
'has_mfa': audit.has_mfa,
|
|
'has_vpn': audit.has_vpn,
|
|
'has_proxmox_pbs': audit.has_proxmox_pbs,
|
|
'has_dr_plan': audit.has_dr_plan,
|
|
'has_mdm': audit.has_mdm,
|
|
# Solutions
|
|
'antivirus_solution': audit.antivirus_solution,
|
|
'backup_solution': audit.backup_solution,
|
|
'monitoring_solution': audit.monitoring_solution,
|
|
'virtualization_platform': audit.virtualization_platform,
|
|
# Collaboration flags
|
|
'open_to_shared_licensing': audit.open_to_shared_licensing,
|
|
'open_to_backup_replication': audit.open_to_backup_replication,
|
|
'open_to_teams_federation': audit.open_to_teams_federation,
|
|
'open_to_shared_monitoring': audit.open_to_shared_monitoring,
|
|
'open_to_collective_purchasing': audit.open_to_collective_purchasing,
|
|
'open_to_knowledge_sharing': audit.open_to_knowledge_sharing,
|
|
# Recommendations
|
|
'recommendations': audit.recommendations
|
|
}
|
|
|
|
# Determine if user can edit audit (user with company edit rights)
|
|
can_edit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('it_audit.html',
|
|
company=company,
|
|
audit_data=audit_data,
|
|
can_edit=can_edit
|
|
)
|
|
|
|
finally:
|
|
db.close()
|