feat(social-audit): add per-company detail view with platform cards
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
New route /admin/social-audit/<company_id> showing detailed social media audit per company: platform cards with metrics, profile checklist, completeness bar, recommendations, invalid profiles section. Added audit detail icon in dashboard table alongside profile link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
286f3235e3
commit
822590cd23
@ -345,3 +345,103 @@ def admin_social_audit():
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SOCIAL MEDIA AUDIT DETAIL (per company)
|
||||
# ============================================================
|
||||
|
||||
@bp.route('/social-audit/<int:company_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_social_audit_detail(company_id):
|
||||
"""Detailed social media audit view for a single company."""
|
||||
if not is_audit_owner():
|
||||
from flask import abort
|
||||
abort(404)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter_by(id=company_id).first()
|
||||
if not company:
|
||||
flash('Firma nie istnieje.', 'error')
|
||||
return redirect(url_for('admin.admin_social_audit'))
|
||||
|
||||
# Get all social media profiles for this company
|
||||
profiles = db.query(CompanySocialMedia).filter(
|
||||
CompanySocialMedia.company_id == company_id
|
||||
).order_by(CompanySocialMedia.platform).all()
|
||||
|
||||
# Separate valid and invalid
|
||||
valid_profiles = [p for p in profiles if p.is_valid]
|
||||
invalid_profiles = [p for p in profiles if not p.is_valid]
|
||||
|
||||
# Build per-platform detail
|
||||
platform_details = []
|
||||
for p in valid_profiles:
|
||||
detail = {
|
||||
'platform': p.platform,
|
||||
'url': p.url,
|
||||
'page_name': p.page_name,
|
||||
'followers_count': p.followers_count or 0,
|
||||
'has_profile_photo': p.has_profile_photo,
|
||||
'has_cover_photo': p.has_cover_photo,
|
||||
'has_bio': p.has_bio,
|
||||
'profile_description': p.profile_description,
|
||||
'posts_count_30d': p.posts_count_30d or 0,
|
||||
'posts_count_365d': p.posts_count_365d or 0,
|
||||
'last_post_date': p.last_post_date,
|
||||
'posting_frequency_score': p.posting_frequency_score or 0,
|
||||
'engagement_rate': float(p.engagement_rate) if p.engagement_rate else 0,
|
||||
'content_types': p.content_types or {},
|
||||
'profile_completeness_score': p.profile_completeness_score or 0,
|
||||
'followers_history': p.followers_history or [],
|
||||
'verified_at': p.verified_at,
|
||||
'source': p.source,
|
||||
'check_status': p.check_status,
|
||||
'is_valid': p.is_valid
|
||||
}
|
||||
platform_details.append(detail)
|
||||
|
||||
# Recommendations
|
||||
recommendations = []
|
||||
platform_names = {p['platform'] for p in platform_details}
|
||||
|
||||
if not platform_details:
|
||||
recommendations.append({'severity': 'critical', 'text': 'Firma nie ma żadnych profili social media.'})
|
||||
else:
|
||||
if 'facebook' not in platform_names:
|
||||
recommendations.append({'severity': 'warning', 'text': 'Brak profilu Facebook — najpopularniejsza platforma wśród firm Izby.'})
|
||||
if 'instagram' not in platform_names:
|
||||
recommendations.append({'severity': 'info', 'text': 'Brak profilu Instagram.'})
|
||||
if 'linkedin' not in platform_names:
|
||||
recommendations.append({'severity': 'warning', 'text': 'Brak profilu LinkedIn — kluczowy dla kontaktów B2B.'})
|
||||
|
||||
for p in platform_details:
|
||||
if p['profile_completeness_score'] > 0 and p['profile_completeness_score'] < 50:
|
||||
recommendations.append({
|
||||
'severity': 'warning',
|
||||
'text': f'{p["platform"].capitalize()}: niska kompletność profilu ({p["profile_completeness_score"]}%). Uzupełnij zdjęcie, opis i dane kontaktowe.'
|
||||
})
|
||||
if p['url'] and 'profile.php?id=' in p['url']:
|
||||
recommendations.append({
|
||||
'severity': 'warning',
|
||||
'text': 'Facebook: adres profilu zawiera profile.php — warto zmienić na niestandardowy URL z nazwą firmy.'
|
||||
})
|
||||
if p['last_post_date']:
|
||||
days_since = (datetime.now() - p['last_post_date']).days
|
||||
if days_since > 90:
|
||||
recommendations.append({
|
||||
'severity': 'warning',
|
||||
'text': f'{p["platform"].capitalize()}: ostatni post {days_since} dni temu. Konto może wyglądać na porzucone.'
|
||||
})
|
||||
|
||||
return render_template('admin/social_audit_detail.html',
|
||||
company=company,
|
||||
platform_details=platform_details,
|
||||
invalid_profiles=invalid_profiles,
|
||||
recommendations=recommendations,
|
||||
now=datetime.now()
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -830,7 +830,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn-icon" title="Zobacz profil">
|
||||
<a href="{{ url_for('admin.admin_social_audit_detail', company_id=company.id) }}" class="btn-icon" title="Szczegóły audytu social media">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn-icon" title="Zobacz profil firmy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
|
||||
517
templates/admin/social_audit_detail.html
Normal file
517
templates/admin/social_audit_detail.html
Normal file
@ -0,0 +1,517 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Audyt Social Media - {{ company.name }} - Norda Biznes Partner{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.audit-detail {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.audit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.audit-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.audit-header .actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Summary strip */
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Recommendations */
|
||||
.recommendations {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.rec-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.rec-item.critical {
|
||||
background: #fef2f2;
|
||||
border-left: 3px solid #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.rec-item.warning {
|
||||
background: #fffbeb;
|
||||
border-left: 3px solid #f59e0b;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.rec-item.info {
|
||||
background: #eff6ff;
|
||||
border-left: 3px solid #3b82f6;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* Platform cards */
|
||||
.platform-section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.platform-detail-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.platform-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
.platform-icon-lg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.platform-icon-lg.facebook { background: #1877f2; }
|
||||
.platform-icon-lg.instagram { background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); }
|
||||
.platform-icon-lg.linkedin { background: #0a66c2; }
|
||||
.platform-icon-lg.youtube { background: #ff0000; }
|
||||
.platform-icon-lg.twitter { background: #000000; }
|
||||
.platform-icon-lg.tiktok { background: #000000; }
|
||||
|
||||
.platform-detail-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.platform-detail-title h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.platform-detail-title a {
|
||||
color: var(--primary);
|
||||
font-size: var(--font-size-sm);
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.platform-detail-title a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.platform-detail-body {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.profile-checklist {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.check-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.check-item.ok { color: #059669; }
|
||||
.check-item.missing { color: #dc2626; }
|
||||
.check-item.unknown { color: var(--text-secondary); }
|
||||
|
||||
.completeness-bar-lg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.completeness-track-lg {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: var(--border-color, #e5e7eb);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.completeness-fill-lg {
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.completeness-fill-lg.high { background: #22c55e; }
|
||||
.completeness-fill-lg.medium { background: #f59e0b; }
|
||||
.completeness-fill-lg.low { background: #ef4444; }
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
flex-wrap: wrap;
|
||||
padding-top: var(--spacing-sm);
|
||||
border-top: 1px solid var(--border-color, #e5e7eb);
|
||||
margin-top: var(--spacing-md);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Invalid profiles */
|
||||
.invalid-section {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.invalid-section h3 {
|
||||
color: #991b1b;
|
||||
margin: 0 0 var(--spacing-md);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.invalid-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-profiles {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl);
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.audit-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.audit-header .actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="audit-detail">
|
||||
<a href="{{ url_for('admin.admin_social_audit') }}" class="back-link">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Powrót do audytu
|
||||
</a>
|
||||
|
||||
<div class="audit-header">
|
||||
<div>
|
||||
<h1>{{ company.name }}</h1>
|
||||
{% if company.website %}
|
||||
<a href="{{ company.website }}" target="_blank" rel="noopener" style="color: var(--primary); font-size: var(--font-size-sm);">{{ company.website }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn btn-outline btn-sm">Profil firmy</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span class="summary-value">{{ platform_details|length }}</span>
|
||||
<span class="summary-label">Platform</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-value">{{ "{:,}".format(platform_details|sum(attribute='followers_count')).replace(",", " ") }}</span>
|
||||
<span class="summary-label">Obserwujących</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
{% set total_posts = platform_details|sum(attribute='posts_count_30d') %}
|
||||
<span class="summary-value">{{ total_posts }}</span>
|
||||
<span class="summary-label">Postów (30 dni)</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
{% set comp_scores = platform_details|selectattr('profile_completeness_score', 'gt', 0)|map(attribute='profile_completeness_score')|list %}
|
||||
{% set avg_comp = (comp_scores|sum / comp_scores|length)|round|int if comp_scores else 0 %}
|
||||
<span class="summary-value {{ 'green' if avg_comp >= 60 else ('yellow' if avg_comp >= 30 else 'red') }}">{{ avg_comp }}%</span>
|
||||
<span class="summary-label">Śr. kompletność</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
{% set eng_rates = platform_details|selectattr('engagement_rate', 'gt', 0)|map(attribute='engagement_rate')|list %}
|
||||
{% set avg_eng = (eng_rates|sum / eng_rates|length)|round(2) if eng_rates else 0 %}
|
||||
<span class="summary-value">{{ avg_eng }}%</span>
|
||||
<span class="summary-label">Śr. engagement</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendations -->
|
||||
{% if recommendations %}
|
||||
<div class="recommendations">
|
||||
<h2 style="font-size: var(--font-size-lg); font-weight: 600; margin-bottom: var(--spacing-sm);">Zalecenia</h2>
|
||||
{% for rec in recommendations %}
|
||||
<div class="rec-item {{ rec.severity }}">
|
||||
{% if rec.severity == 'critical' %}
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
||||
{% elif rec.severity == 'warning' %}
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
||||
{% else %}
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{% endif %}
|
||||
<span>{{ rec.text }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Platform Details -->
|
||||
<div class="platform-section">
|
||||
<h2 style="font-size: var(--font-size-lg); font-weight: 600; margin-bottom: var(--spacing-md);">Profile social media</h2>
|
||||
|
||||
{% if platform_details %}
|
||||
{% set platform_icons = {
|
||||
'facebook': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>',
|
||||
'instagram': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.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.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
|
||||
'linkedin': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 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"/></svg>',
|
||||
'youtube': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
|
||||
'twitter': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>',
|
||||
'tiktok': '<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>'
|
||||
} %}
|
||||
|
||||
{% for p in platform_details %}
|
||||
<div class="platform-detail-card">
|
||||
<div class="platform-detail-header">
|
||||
<div class="platform-icon-lg {{ p.platform }}">
|
||||
{{ platform_icons[p.platform]|safe }}
|
||||
</div>
|
||||
<div class="platform-detail-title">
|
||||
<h3>{{ p.platform|capitalize }}{% if p.page_name %} — {{ p.page_name }}{% endif %}</h3>
|
||||
<a href="{{ p.url }}" target="_blank" rel="noopener">{{ p.url }}</a>
|
||||
</div>
|
||||
{% if p.check_status == 'needs_verification' %}
|
||||
<span style="background: #fef3c7; color: #b45309; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500;">Do weryfikacji</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="platform-detail-body">
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<div class="metric-value">{{ "{:,}".format(p.followers_count).replace(",", " ") }}</div>
|
||||
<div class="metric-label">Obserwujących</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">{{ p.engagement_rate }}%</div>
|
||||
<div class="metric-label">Engagement rate</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">{{ p.posts_count_30d }}</div>
|
||||
<div class="metric-label">Postów (30 dni)</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">{{ p.posts_count_365d }}</div>
|
||||
<div class="metric-label">Postów (rok)</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">{{ p.posting_frequency_score }}/10</div>
|
||||
<div class="metric-label">Regularność</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">
|
||||
{% if p.last_post_date %}
|
||||
{{ p.last_post_date.strftime('%d.%m.%Y') }}
|
||||
{% else %}
|
||||
<span style="color: var(--text-secondary);">b/d</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metric-label">Ostatni post</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile completeness -->
|
||||
{% if p.profile_completeness_score > 0 %}
|
||||
<div style="margin-bottom: var(--spacing-md);">
|
||||
<span style="font-size: var(--font-size-sm); font-weight: 500;">Kompletność profilu: {{ p.profile_completeness_score }}%</span>
|
||||
<div class="completeness-bar-lg">
|
||||
<div class="completeness-track-lg">
|
||||
<div class="completeness-fill-lg {{ 'high' if p.profile_completeness_score >= 60 else ('medium' if p.profile_completeness_score >= 30 else 'low') }}"
|
||||
style="width: {{ p.profile_completeness_score }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Profile checklist -->
|
||||
<div class="profile-checklist">
|
||||
{% if p.has_profile_photo is not none %}
|
||||
<span class="check-item {{ 'ok' if p.has_profile_photo else 'missing' }}">
|
||||
{{ '✓' if p.has_profile_photo else '✗' }} Zdjęcie profilowe
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="check-item unknown">? Zdjęcie profilowe</span>
|
||||
{% endif %}
|
||||
|
||||
{% if p.has_cover_photo is not none %}
|
||||
<span class="check-item {{ 'ok' if p.has_cover_photo else 'missing' }}">
|
||||
{{ '✓' if p.has_cover_photo else '✗' }} Zdjęcie w tle
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="check-item unknown">? Zdjęcie w tle</span>
|
||||
{% endif %}
|
||||
|
||||
{% if p.has_bio is not none %}
|
||||
<span class="check-item {{ 'ok' if p.has_bio else 'missing' }}">
|
||||
{{ '✓' if p.has_bio else '✗' }} Opis / bio
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="check-item unknown">? Opis / bio</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if p.profile_description %}
|
||||
<div style="margin-top: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: var(--background); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||||
{{ p.profile_description[:300] }}{% if p.profile_description|length > 300 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if p.content_types %}
|
||||
<div style="margin-top: var(--spacing-sm); display: flex; gap: var(--spacing-sm); flex-wrap: wrap;">
|
||||
{% for ctype, count in p.content_types.items() %}
|
||||
<span style="background: var(--background); padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11px; color: var(--text-secondary);">
|
||||
{{ ctype }}: {{ count }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="meta-info">
|
||||
<span>Źródło: {{ p.source or 'nieznane' }}</span>
|
||||
{% if p.verified_at %}
|
||||
<span>Zweryfikowano: {{ p.verified_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
{% endif %}
|
||||
<span>Status: {{ p.check_status or 'ok' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-profiles">
|
||||
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="margin-bottom: var(--spacing-md); opacity: 0.3;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||
</svg>
|
||||
<p>Firma nie ma żadnych profili social media.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Invalid profiles -->
|
||||
{% if invalid_profiles %}
|
||||
<div class="invalid-section">
|
||||
<h3>Nieaktywne / nieważne profile ({{ invalid_profiles|length }})</h3>
|
||||
{% for p in invalid_profiles %}
|
||||
<div class="invalid-item">
|
||||
<span style="font-weight: 500; min-width: 80px; text-transform: capitalize;">{{ p.platform }}</span>
|
||||
<a href="{{ p.url }}" target="_blank" rel="noopener" style="color: #7f1d1d;">{{ p.url|truncate(60) }}</a>
|
||||
<span style="margin-left: auto; font-size: 11px;">{{ p.check_status or 'invalid' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user