nordabiz/templates/admin/social_audit_detail.html
Maciej Pienczyn 822590cd23
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
feat(social-audit): add per-company detail view with platform cards
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>
2026-03-11 22:10:31 +01:00

518 lines
22 KiB
HTML

{% 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' }}">
{{ '&#10003;' if p.has_profile_photo else '&#10007;' }} 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' }}">
{{ '&#10003;' if p.has_cover_photo else '&#10007;' }} 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' }}">
{{ '&#10003;' if p.has_bio else '&#10007;' }} 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 %}