feat: Add URL normalization and inline audit sections
- Add normalize_social_url() function to database.py to prevent www vs non-www duplicates in social media records - Update update_social_media.py to normalize URLs before insert - Update social_media_audit.py to normalize URLs before insert - Add inline GBP Audit section to company profile - Add inline Social Media Audit section to company profile - Add inline IT Audit section to company profile Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
91fea3ba2c
commit
986360f7d5
37
app.py
37
app.py
@ -107,7 +107,9 @@ from database import (
|
||||
MembershipFee,
|
||||
MembershipFeeConfig,
|
||||
Person,
|
||||
CompanyPerson
|
||||
CompanyPerson,
|
||||
GBPAudit,
|
||||
ITAudit
|
||||
)
|
||||
|
||||
# Import services
|
||||
@ -632,6 +634,16 @@ def company_detail(company_id):
|
||||
Person.nazwisko
|
||||
).all()
|
||||
|
||||
# Load GBP audit (most recent)
|
||||
gbp_audit = db.query(GBPAudit).filter_by(
|
||||
company_id=company_id
|
||||
).order_by(GBPAudit.audit_date.desc()).first()
|
||||
|
||||
# Load IT audit (most recent)
|
||||
it_audit = db.query(ITAudit).filter_by(
|
||||
company_id=company_id
|
||||
).order_by(ITAudit.audit_date.desc()).first()
|
||||
|
||||
return render_template('company_detail.html',
|
||||
company=company,
|
||||
maturity_data=maturity_data,
|
||||
@ -643,7 +655,9 @@ def company_detail(company_id):
|
||||
social_media=social_media,
|
||||
contacts=contacts,
|
||||
recommendations=recommendations,
|
||||
people=people
|
||||
people=people,
|
||||
gbp_audit=gbp_audit,
|
||||
it_audit=it_audit
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@ -8109,7 +8123,8 @@ def admin_zopk_news_approve(news_id):
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
logger.error(f"Error approving ZOPK news {news_id}: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas zatwierdzania'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -8143,7 +8158,8 @@ def admin_zopk_news_reject(news_id):
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
logger.error(f"Error rejecting ZOPK news {news_id}: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas odrzucania'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -8274,7 +8290,8 @@ def admin_zopk_reject_old_news():
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
logger.error(f"Error rejecting old ZOPK news: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas odrzucania starych newsów'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -8308,7 +8325,8 @@ def admin_zopk_evaluate_ai():
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
logger.error(f"Error evaluating ZOPK news with AI: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas oceny AI'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -8342,7 +8360,8 @@ def admin_zopk_reevaluate_scores():
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
logger.error(f"Error reevaluating ZOPK news scores: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas ponownej oceny'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -8425,13 +8444,13 @@ def api_zopk_search_news():
|
||||
# Update job status on error
|
||||
try:
|
||||
fetch_job.status = 'failed'
|
||||
fetch_job.error_message = str(e)
|
||||
fetch_job.error_message = str(e) # Keep internal log
|
||||
fetch_job.completed_at = datetime.now()
|
||||
db.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas wyszukiwania newsów'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
51
database.py
51
database.py
@ -45,6 +45,57 @@ DATABASE_URL = os.getenv(
|
||||
IS_SQLITE = DATABASE_URL.startswith('sqlite')
|
||||
|
||||
|
||||
def normalize_social_url(url: str, platform: str = None) -> str:
|
||||
"""
|
||||
Normalize social media URLs to prevent duplicates.
|
||||
|
||||
Handles:
|
||||
- www vs non-www (removes www.)
|
||||
- http vs https (forces https)
|
||||
- Trailing slashes (removes)
|
||||
- Platform-specific canonicalization
|
||||
|
||||
Examples:
|
||||
normalize_social_url('http://www.facebook.com/inpipl/')
|
||||
-> 'https://facebook.com/inpipl'
|
||||
|
||||
normalize_social_url('https://www.instagram.com/user/')
|
||||
-> 'https://instagram.com/user'
|
||||
"""
|
||||
if not url:
|
||||
return url
|
||||
|
||||
url = url.strip()
|
||||
|
||||
# Force https
|
||||
if url.startswith('http://'):
|
||||
url = 'https://' + url[7:]
|
||||
elif not url.startswith('https://'):
|
||||
url = 'https://' + url
|
||||
|
||||
# Remove www. prefix
|
||||
url = url.replace('https://www.', 'https://')
|
||||
|
||||
# Remove trailing slash
|
||||
url = url.rstrip('/')
|
||||
|
||||
# Platform-specific normalization
|
||||
if platform == 'facebook' or 'facebook.com' in url:
|
||||
# fb.com -> facebook.com
|
||||
url = url.replace('https://fb.com/', 'https://facebook.com/')
|
||||
url = url.replace('https://m.facebook.com/', 'https://facebook.com/')
|
||||
|
||||
if platform == 'twitter' or 'twitter.com' in url or 'x.com' in url:
|
||||
# x.com -> twitter.com (or vice versa, pick one canonical)
|
||||
url = url.replace('https://x.com/', 'https://twitter.com/')
|
||||
|
||||
if platform == 'linkedin' or 'linkedin.com' in url:
|
||||
# Remove locale prefix
|
||||
url = url.replace('/pl/', '/').replace('/en/', '/')
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class StringArray(TypeDecorator):
|
||||
"""
|
||||
Platform-agnostic array type.
|
||||
|
||||
@ -54,6 +54,25 @@ import whois
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
try:
|
||||
from database import normalize_social_url
|
||||
except ImportError:
|
||||
# Fallback: define locally if import fails
|
||||
def normalize_social_url(url: str, platform: str = None) -> str:
|
||||
"""Normalize social media URLs to prevent duplicates."""
|
||||
if not url:
|
||||
return url
|
||||
url = url.strip()
|
||||
if url.startswith('http://'):
|
||||
url = 'https://' + url[7:]
|
||||
elif not url.startswith('https://'):
|
||||
url = 'https://' + url
|
||||
url = url.replace('https://www.', 'https://')
|
||||
url = url.rstrip('/')
|
||||
return url
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@ -1097,6 +1116,9 @@ class SocialMediaAuditor:
|
||||
|
||||
# Save social media
|
||||
for platform, url in result.get('social_media', {}).items():
|
||||
# Normalize URL to prevent www vs non-www duplicates
|
||||
normalized_url = normalize_social_url(url, platform)
|
||||
|
||||
upsert_social = text("""
|
||||
INSERT INTO company_social_media (
|
||||
company_id, platform, url, verified_at, source, is_valid
|
||||
@ -1112,7 +1134,7 @@ class SocialMediaAuditor:
|
||||
session.execute(upsert_social, {
|
||||
'company_id': company_id,
|
||||
'platform': platform,
|
||||
'url': url,
|
||||
'url': normalized_url,
|
||||
'verified_at': result['audit_date'],
|
||||
'source': 'website_scrape',
|
||||
'is_valid': True,
|
||||
|
||||
@ -2261,6 +2261,441 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- GBP Audit Section - Only show if GBP audit was performed -->
|
||||
{% if gbp_audit and gbp_audit.completeness_score is not none %}
|
||||
<div class="company-section" id="gbp-audit">
|
||||
<h2 class="section-title">
|
||||
Audyt Google Business Profile
|
||||
<span style="font-size: var(--font-size-sm); font-weight: normal; color: var(--text-secondary); margin-left: var(--spacing-sm);">
|
||||
({{ gbp_audit.audit_date.strftime('%d.%m.%Y') }})
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<!-- GBP Score Banner -->
|
||||
<div style="margin-bottom: var(--spacing-lg); padding: var(--spacing-lg); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-lg);
|
||||
background: linear-gradient(135deg, {% if gbp_audit.completeness_score >= 90 %}#10b981, #059669{% elif gbp_audit.completeness_score >= 50 %}#f59e0b, #d97706{% else %}#ef4444, #dc2626{% endif %});">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
|
||||
<span style="font-size: 32px; font-weight: 700; color: white;">{{ gbp_audit.completeness_score }}</span>
|
||||
</div>
|
||||
<div style="flex: 1; color: white;">
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 600; margin-bottom: 4px;">
|
||||
{% if gbp_audit.completeness_score >= 90 %}Doskonały profil GBP{% elif gbp_audit.completeness_score >= 70 %}Dobry profil GBP{% elif gbp_audit.completeness_score >= 50 %}Przeciętny profil GBP{% else %}Profil GBP wymaga uzupełnienia{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); opacity: 0.9;">
|
||||
{% if gbp_audit.completeness_score >= 90 %}Profil Google Business jest kompletny i dobrze zoptymalizowany{% elif gbp_audit.completeness_score >= 70 %}Profil jest dobrze uzupełniony, ale można go jeszcze ulepszyć{% elif gbp_audit.completeness_score >= 50 %}Profil wymaga uzupełnienia kilku ważnych informacji{% else %}Profil wymaga uzupełnienia podstawowych danych{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GBP Stats Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-md); margin-bottom: var(--spacing-lg);">
|
||||
<!-- Reviews -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center; border: 2px solid #e5e7eb;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm); background: #fef3c7; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="#92400e" viewBox="0 0 24 24">
|
||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #92400e;">
|
||||
{% if gbp_audit.review_count is not none %}{{ gbp_audit.review_count }}{% else %}-{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Opinii</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center;
|
||||
border: 2px solid {% if gbp_audit.average_rating and gbp_audit.average_rating|float >= 4.5 %}#10b981{% elif gbp_audit.average_rating and gbp_audit.average_rating|float >= 3.5 %}#f59e0b{% elif gbp_audit.average_rating %}#ef4444{% else %}#e5e7eb{% endif %};">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm);
|
||||
background: {% if gbp_audit.average_rating and gbp_audit.average_rating|float >= 4.5 %}#dcfce7{% elif gbp_audit.average_rating and gbp_audit.average_rating|float >= 3.5 %}#fef3c7{% elif gbp_audit.average_rating %}#fee2e2{% else %}#f3f4f6{% endif %};
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{% if gbp_audit.average_rating and gbp_audit.average_rating|float >= 4.5 %}#166534{% elif gbp_audit.average_rating and gbp_audit.average_rating|float >= 3.5 %}#92400e{% elif gbp_audit.average_rating %}#991b1b{% else %}#9ca3af{% endif %}" viewBox="0 0 24 24">
|
||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: {% if gbp_audit.average_rating and gbp_audit.average_rating|float >= 4.5 %}#166534{% elif gbp_audit.average_rating and gbp_audit.average_rating|float >= 3.5 %}#92400e{% elif gbp_audit.average_rating %}#991b1b{% else %}#9ca3af{% endif %};">
|
||||
{% if gbp_audit.average_rating is not none %}{{ gbp_audit.average_rating|round(1) }}{% else %}-{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Średnia ocen</div>
|
||||
</div>
|
||||
|
||||
<!-- Photos -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center; border: 2px solid #e5e7eb;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm); background: #dbeafe; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="#1e40af" viewBox="0 0 24 24">
|
||||
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: #1e40af;">
|
||||
{% if gbp_audit.photo_count is not none %}{{ gbp_audit.photo_count }}{% else %}-{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Zdjęć</div>
|
||||
</div>
|
||||
|
||||
<!-- Completeness -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center;
|
||||
border: 2px solid {% if gbp_audit.completeness_score >= 90 %}#10b981{% elif gbp_audit.completeness_score >= 50 %}#f59e0b{% else %}#ef4444{% endif %};">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm);
|
||||
background: {% if gbp_audit.completeness_score >= 90 %}#dcfce7{% elif gbp_audit.completeness_score >= 50 %}#fef3c7{% else %}#fee2e2{% endif %};
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{% if gbp_audit.completeness_score >= 90 %}#166534{% elif gbp_audit.completeness_score >= 50 %}#92400e{% else %}#991b1b{% endif %}" viewBox="0 0 24 24">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: {% if gbp_audit.completeness_score >= 90 %}#166534{% elif gbp_audit.completeness_score >= 50 %}#92400e{% else %}#991b1b{% endif %};">
|
||||
{{ gbp_audit.completeness_score }}%
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Kompletność</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Checklist -->
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-lg);">
|
||||
<!-- Basic Info -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 1px solid var(--border);">
|
||||
<h3 style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||||
<svg width="20" height="20" fill="var(--primary)" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
||||
</svg>
|
||||
Informacje podstawowe
|
||||
</h3>
|
||||
<div style="display: grid; gap: var(--spacing-sm);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Nazwa firmy</span>
|
||||
{% if gbp_audit.has_name %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Adres</span>
|
||||
{% if gbp_audit.has_address %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Telefon</span>
|
||||
{% if gbp_audit.has_phone %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Strona www</span>
|
||||
{% if gbp_audit.has_website %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Godziny otwarcia</span>
|
||||
{% if gbp_audit.has_hours %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content & Engagement -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 1px solid var(--border);">
|
||||
<h3 style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||||
<svg width="20" height="20" fill="var(--primary)" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
||||
</svg>
|
||||
Treści i zaangażowanie
|
||||
</h3>
|
||||
<div style="display: grid; gap: var(--spacing-sm);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Opis firmy</span>
|
||||
{% if gbp_audit.has_description %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Kategorie</span>
|
||||
{% if gbp_audit.has_categories %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Usługi/produkty</span>
|
||||
{% if gbp_audit.has_services %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">OK</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fef3c7; color: #92400e; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Zdjęcia</span>
|
||||
{% if gbp_audit.has_photos %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">{{ gbp_audit.photo_count or 'OK' }}</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Opinie klientów</span>
|
||||
{% if gbp_audit.has_reviews %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">{{ gbp_audit.review_count }}</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fef3c7; color: #92400e; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Brak</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if gbp_audit.google_maps_url %}
|
||||
<div style="margin-top: var(--spacing-lg); text-align: center;">
|
||||
<a href="{{ gbp_audit.google_maps_url }}" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-lg); background: var(--primary); color: white; text-decoration: none; border-radius: var(--radius); font-weight: 500;">
|
||||
<svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||
</svg>
|
||||
Zobacz w Google Maps
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Social Media Audit Section -->
|
||||
{% if social_media and social_media|length > 0 %}
|
||||
<div class="company-section" id="social-media-audit">
|
||||
<h2 class="section-title">
|
||||
Audyt Social Media
|
||||
</h2>
|
||||
|
||||
<!-- Social Media Summary Banner -->
|
||||
{% set active_platforms = social_media|selectattr('is_valid', 'equalto', true)|list|length %}
|
||||
{% set total_followers = social_media|selectattr('followers_count')|sum(attribute='followers_count') %}
|
||||
<div style="margin-bottom: var(--spacing-lg); padding: var(--spacing-lg); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-lg);
|
||||
background: linear-gradient(135deg, {% if active_platforms >= 4 %}#10b981, #059669{% elif active_platforms >= 2 %}#3b82f6, #2563eb{% else %}#f59e0b, #d97706{% endif %});">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
|
||||
<span style="font-size: 32px; font-weight: 700; color: white;">{{ active_platforms }}</span>
|
||||
</div>
|
||||
<div style="flex: 1; color: white;">
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 600; margin-bottom: 4px;">
|
||||
{% if active_platforms >= 4 %}Doskonała obecność w Social Media{% elif active_platforms >= 2 %}Dobra obecność w Social Media{% else %}Podstawowa obecność w Social Media{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); opacity: 0.9;">
|
||||
{{ active_platforms }} aktywn{{ 'a platforma' if active_platforms == 1 else ('e platformy' if active_platforms in [2, 3, 4] else 'ych platform') }}
|
||||
{% if total_followers > 0 %} · {{ "{:,}".format(total_followers|int).replace(",", " ") }} obserwujących łącznie{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Platforms Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-md);">
|
||||
{% set platform_icons = {
|
||||
'facebook': {'icon': '<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>', 'color': '#1877F2'},
|
||||
'instagram': {'icon': '<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>', 'color': '#E4405F'},
|
||||
'linkedin': {'icon': '<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>', 'color': '#0A66C2'},
|
||||
'youtube': {'icon': '<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>', 'color': '#FF0000'},
|
||||
'twitter': {'icon': '<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>', 'color': '#000000'},
|
||||
'tiktok': {'icon': '<path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/>', 'color': '#000000'}
|
||||
} %}
|
||||
|
||||
{% for sm in social_media %}
|
||||
<a href="{{ sm.url }}" target="_blank" rel="noopener" style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center; border: 1px solid var(--border); text-decoration: none; color: inherit; transition: var(--transition);">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm); background: {{ platform_icons[sm.platform]['color'] if sm.platform in platform_icons else 'var(--primary)' }}20; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{{ platform_icons[sm.platform]['color'] if sm.platform in platform_icons else 'var(--primary)' }}" viewBox="0 0 24 24">
|
||||
{{ platform_icons[sm.platform]['icon']|safe if sm.platform in platform_icons else '<circle cx="12" cy="12" r="10"/>' }}
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: 2px; text-transform: capitalize;">{{ sm.platform }}</div>
|
||||
{% if sm.followers_count %}
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ "{:,}".format(sm.followers_count|int).replace(",", " ") }} obserwujących</div>
|
||||
{% elif sm.page_name %}
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ sm.page_name|truncate(20) }}</div>
|
||||
{% else %}
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-muted);">Aktywny profil</div>
|
||||
{% endif %}
|
||||
{% if sm.is_valid %}
|
||||
<span style="display: inline-block; margin-top: var(--spacing-xs); padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 11px; font-weight: 500;">Zweryfikowany</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- IT Audit Section - Only show if IT audit was performed -->
|
||||
{% if it_audit and it_audit.overall_score is not none %}
|
||||
<div class="company-section" id="it-audit">
|
||||
<h2 class="section-title">
|
||||
Audyt IT
|
||||
<span style="font-size: var(--font-size-sm); font-weight: normal; color: var(--text-secondary); margin-left: var(--spacing-sm);">
|
||||
({{ it_audit.audit_date.strftime('%d.%m.%Y') }})
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<!-- IT Score Banner -->
|
||||
<div style="margin-bottom: var(--spacing-lg); padding: var(--spacing-lg); border-radius: var(--radius-lg); display: flex; align-items: center; gap: var(--spacing-lg);
|
||||
background: linear-gradient(135deg, {% if it_audit.overall_score >= 90 %}#10b981, #059669{% elif it_audit.overall_score >= 50 %}#6366f1, #4f46e5{% else %}#f59e0b, #d97706{% endif %});">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
|
||||
<span style="font-size: 32px; font-weight: 700; color: white;">{{ it_audit.overall_score }}</span>
|
||||
</div>
|
||||
<div style="flex: 1; color: white;">
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 600; margin-bottom: 4px;">
|
||||
{% if it_audit.maturity_level == 'advanced' %}Zaawansowana infrastruktura IT{% elif it_audit.maturity_level == 'established' %}Rozwinięta infrastruktura IT{% elif it_audit.maturity_level == 'developing' %}Rozwijająca się infrastruktura IT{% else %}Podstawowa infrastruktura IT{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); opacity: 0.9;">
|
||||
{% if it_audit.overall_score >= 90 %}Firma posiada nowoczesną i dobrze zabezpieczoną infrastrukturę{% elif it_audit.overall_score >= 70 %}Infrastruktura IT na dobrym poziomie z potencjałem do rozwoju{% elif it_audit.overall_score >= 50 %}Infrastruktura IT wymaga modernizacji w niektórych obszarach{% else %}Infrastruktura IT wymaga znaczącej rozbudowy{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IT Scores Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-md); margin-bottom: var(--spacing-lg);">
|
||||
<!-- Overall Score -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center;
|
||||
border: 2px solid {% if it_audit.overall_score >= 90 %}#10b981{% elif it_audit.overall_score >= 50 %}#6366f1{% else %}#f59e0b{% endif %};">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm);
|
||||
background: {% if it_audit.overall_score >= 90 %}#dcfce7{% elif it_audit.overall_score >= 50 %}#e0e7ff{% else %}#fef3c7{% endif %};
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{% if it_audit.overall_score >= 90 %}#166534{% elif it_audit.overall_score >= 50 %}#4338ca{% else %}#92400e{% endif %}" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: {% if it_audit.overall_score >= 90 %}#166534{% elif it_audit.overall_score >= 50 %}#4338ca{% else %}#92400e{% endif %};">
|
||||
{{ it_audit.overall_score }}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Ogólny wynik</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Score -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center;
|
||||
border: 2px solid {% if it_audit.security_score and it_audit.security_score >= 90 %}#10b981{% elif it_audit.security_score and it_audit.security_score >= 50 %}#f59e0b{% elif it_audit.security_score %}#ef4444{% else %}#e5e7eb{% endif %};">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm);
|
||||
background: {% if it_audit.security_score and it_audit.security_score >= 90 %}#dcfce7{% elif it_audit.security_score and it_audit.security_score >= 50 %}#fef3c7{% elif it_audit.security_score %}#fee2e2{% else %}#f3f4f6{% endif %};
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{% if it_audit.security_score and it_audit.security_score >= 90 %}#166534{% elif it_audit.security_score and it_audit.security_score >= 50 %}#92400e{% elif it_audit.security_score %}#991b1b{% else %}#9ca3af{% endif %}" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: {% if it_audit.security_score and it_audit.security_score >= 90 %}#166534{% elif it_audit.security_score and it_audit.security_score >= 50 %}#92400e{% elif it_audit.security_score %}#991b1b{% else %}#9ca3af{% endif %};">
|
||||
{% if it_audit.security_score is not none %}{{ it_audit.security_score }}{% else %}-{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Bezpieczeństwo</div>
|
||||
</div>
|
||||
|
||||
<!-- Collaboration Score -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center;
|
||||
border: 2px solid {% if it_audit.collaboration_score and it_audit.collaboration_score >= 90 %}#10b981{% elif it_audit.collaboration_score and it_audit.collaboration_score >= 50 %}#3b82f6{% elif it_audit.collaboration_score %}#f59e0b{% else %}#e5e7eb{% endif %};">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm);
|
||||
background: {% if it_audit.collaboration_score and it_audit.collaboration_score >= 90 %}#dcfce7{% elif it_audit.collaboration_score and it_audit.collaboration_score >= 50 %}#dbeafe{% elif it_audit.collaboration_score %}#fef3c7{% else %}#f3f4f6{% endif %};
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="{% if it_audit.collaboration_score and it_audit.collaboration_score >= 90 %}#166534{% elif it_audit.collaboration_score and it_audit.collaboration_score >= 50 %}#1e40af{% elif it_audit.collaboration_score %}#92400e{% else %}#9ca3af{% endif %}" viewBox="0 0 24 24">
|
||||
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 28px; font-weight: 700; color: {% if it_audit.collaboration_score and it_audit.collaboration_score >= 90 %}#166534{% elif it_audit.collaboration_score and it_audit.collaboration_score >= 50 %}#1e40af{% elif it_audit.collaboration_score %}#92400e{% else %}#9ca3af{% endif %};">
|
||||
{% if it_audit.collaboration_score is not none %}{{ it_audit.collaboration_score }}{% else %}-{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Współpraca</div>
|
||||
</div>
|
||||
|
||||
<!-- Maturity Level -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); text-align: center; border: 2px solid #e5e7eb;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; margin: 0 auto var(--spacing-sm); background: #f3f4f6; display: flex; align-items: center; justify-content: center;">
|
||||
<svg width="24" height="24" fill="#6b7280" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l4.59-4.58L18 11l-6 6z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size: 18px; font-weight: 700; color: var(--text-primary);">
|
||||
{% if it_audit.maturity_level == 'advanced' %}Zaawansowany{% elif it_audit.maturity_level == 'established' %}Rozwinięty{% elif it_audit.maturity_level == 'developing' %}Rozwijający{% else %}Podstawowy{% endif %}
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); font-weight: 500;">Poziom dojrzałości</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IT Capabilities Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-lg);">
|
||||
<!-- Cloud & Identity -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 1px solid var(--border);">
|
||||
<h3 style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||||
<svg width="20" height="20" fill="var(--primary)" viewBox="0 0 24 24">
|
||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/>
|
||||
</svg>
|
||||
Chmura i tożsamość
|
||||
</h3>
|
||||
<div style="display: grid; gap: var(--spacing-sm);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Microsoft 365</span>
|
||||
{% if it_audit.has_m365 %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #f3f4f6; color: #9ca3af; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Azure AD / Entra ID</span>
|
||||
{% if it_audit.has_azure_ad %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #f3f4f6; color: #9ca3af; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">Google Workspace</span>
|
||||
{% if it_audit.has_google_workspace %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #f3f4f6; color: #9ca3af; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security -->
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 1px solid var(--border);">
|
||||
<h3 style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||||
<svg width="20" height="20" fill="var(--primary)" viewBox="0 0 24 24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
|
||||
</svg>
|
||||
Bezpieczeństwo
|
||||
</h3>
|
||||
<div style="display: grid; gap: var(--spacing-sm);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">MFA (2FA)</span>
|
||||
{% if it_audit.has_mfa %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fee2e2; color: #991b1b; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">VPN</span>
|
||||
{% if it_audit.has_vpn %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #f3f4f6; color: #9ca3af; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-sm); background: white; border-radius: var(--radius);">
|
||||
<span style="font-size: var(--font-size-sm); color: var(--text-secondary);">EDR</span>
|
||||
{% if it_audit.has_edr %}
|
||||
<span style="padding: 2px 8px; background: #dcfce7; color: #166534; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Tak</span>
|
||||
{% else %}
|
||||
<span style="padding: 2px 8px; background: #fef3c7; color: #92400e; border-radius: var(--radius-sm); font-size: 12px; font-weight: 500;">Nie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Company Events - UKRYTE (2026-01-11) - do przywrócenia w przyszłości
|
||||
{% if events %}
|
||||
<div class="company-section">
|
||||
|
||||
@ -18,7 +18,7 @@ from datetime import datetime
|
||||
# The database module will fall back to a safe placeholder if not set.
|
||||
# NEVER commit real credentials to version control (CWE-798).
|
||||
|
||||
from database import SessionLocal, Company, CompanySocialMedia
|
||||
from database import SessionLocal, Company, CompanySocialMedia, normalize_social_url
|
||||
from sqlalchemy import func
|
||||
|
||||
PLATFORMS = ['facebook', 'instagram', 'youtube', 'linkedin', 'tiktok', 'twitter']
|
||||
@ -63,14 +63,19 @@ def update_social_media(dry_run=False):
|
||||
if not url:
|
||||
continue
|
||||
|
||||
# Check if profile already exists
|
||||
# Normalize URL to prevent www vs non-www duplicates
|
||||
url = normalize_social_url(url, platform)
|
||||
|
||||
# Check if profile already exists for this platform
|
||||
existing = db.query(CompanySocialMedia).filter(
|
||||
CompanySocialMedia.company_id == company_id,
|
||||
CompanySocialMedia.platform == platform
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
if existing.url != url:
|
||||
# Normalize existing URL for comparison
|
||||
existing_normalized = normalize_social_url(existing.url, platform)
|
||||
if existing_normalized != url:
|
||||
old_url = existing.url
|
||||
if not dry_run:
|
||||
existing.url = url
|
||||
|
||||
Loading…
Reference in New Issue
Block a user