nordabiz/templates/social_audit.html
Maciej Pienczyn 7197af3933
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(audit): Add previous vs current AI analysis comparison
Store previous analysis before regeneration and show comparison table
with priority breakdown, new/removed actions diff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 18:05:17 +01:00

1586 lines
68 KiB
HTML

{% extends "base.html" %}
{% block title %}Audyt Social Media - {{ company.name }} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.audit-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.audit-header-info h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.audit-header-info p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.data-source-info {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
background: rgba(168, 85, 247, 0.1);
border-radius: var(--radius);
font-size: var(--font-size-sm);
color: #a855f7;
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
/* Score Section */
.score-section {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
@media (max-width: 768px) {
.score-section {
grid-template-columns: 1fr;
text-align: center;
}
}
.score-circle {
width: 180px;
height: 180px;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background: conic-gradient(
var(--score-color, var(--secondary)) calc(var(--score-percent, 0) * 3.6deg),
#e2e8f0 0deg
);
margin: 0 auto;
}
.score-circle::before {
content: '';
position: absolute;
width: 150px;
height: 150px;
border-radius: 50%;
background: var(--surface);
}
.score-value {
position: relative;
z-index: 1;
font-size: 3rem;
font-weight: 700;
line-height: 1;
}
/* Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green */
.score-value.score-excellent { color: #10b981; }
.score-value.score-good { color: #84cc16; }
.score-value.score-average { color: #f59e0b; }
.score-value.score-needs-work { color: #f97316; }
.score-value.score-poor { color: #ef4444; }
.score-label {
position: relative;
z-index: 1;
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.score-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.score-category {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--spacing-sm);
}
/* Unified 5-level color scale */
.score-category.excellent { color: #10b981; }
.score-category.good { color: #84cc16; }
.score-category.average { color: #f59e0b; }
.score-category.needs-work { color: #f97316; }
.score-category.poor { color: #ef4444; }
.score-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.6;
margin-bottom: var(--spacing-md);
}
.platforms-summary {
display: flex;
gap: var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
flex-wrap: wrap;
}
.platforms-summary-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.platforms-summary-item svg {
width: 16px;
height: 16px;
}
.platforms-summary-item.found { color: var(--success); }
.platforms-summary-item.missing { color: var(--text-secondary); }
/* Section Title */
.section-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
.section-title svg {
width: 24px;
height: 24px;
color: #a855f7;
}
/* Platforms Grid */
.platforms-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.platform-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
border-left: 4px solid;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.platform-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.platform-card.found {
border-left-color: var(--success);
}
.platform-card.missing {
border-left-color: var(--text-tertiary);
opacity: 0.7;
}
.platform-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.platform-icon {
width: 40px;
height: 40px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.platform-icon.facebook { background: #1877f2; color: white; }
.platform-icon.instagram { background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); color: white; }
.platform-icon.linkedin { background: #0a66c2; color: white; }
.platform-icon.youtube { background: #ff0000; color: white; }
.platform-icon.twitter { background: #000000; color: white; }
.platform-icon.tiktok { background: #000000; color: white; }
.platform-name {
font-weight: 600;
font-size: var(--font-size-base);
color: var(--text-primary);
}
.platform-status {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
padding: 2px 8px;
border-radius: var(--radius);
}
.platform-status.active {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.platform-status.inactive {
background: rgba(107, 114, 128, 0.1);
color: var(--text-tertiary);
}
.platform-status.needs-verification {
background: rgba(245, 158, 11, 0.1);
color: #b45309;
}
.platform-details {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.platform-url {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
word-break: break-all;
}
.platform-url a {
color: var(--primary);
text-decoration: none;
}
.platform-url a:hover {
text-decoration: underline;
}
.platform-meta {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
flex-wrap: wrap;
}
.platform-meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.platform-meta-item svg {
width: 14px;
height: 14px;
}
.platform-missing-text {
color: var(--text-tertiary);
font-style: italic;
}
/* No data state */
.no-data {
text-align: center;
padding: var(--spacing-2xl);
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.no-data svg {
width: 64px;
height: 64px;
color: var(--text-tertiary);
margin-bottom: var(--spacing-md);
}
.no-data h3 {
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.no-data p {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
font-weight: 500;
font-size: var(--font-size-sm);
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
text-decoration: none;
}
.btn svg {
width: 16px;
height: 16px;
}
.btn-primary {
background: #a855f7;
color: white;
border-color: #a855f7;
}
.btn-primary:hover {
background: #9333ea;
border-color: #9333ea;
}
.btn-outline {
background: transparent;
color: var(--text-secondary);
border-color: var(--border);
}
.btn-outline:hover {
background: var(--background);
color: var(--text-primary);
}
.btn-sm {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-xs);
}
/* Recommendations */
.recommendations {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
}
.recommendation-item {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border);
}
.recommendation-item:last-child {
border-bottom: none;
}
.recommendation-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 12px;
}
.recommendation-icon.priority-high {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.recommendation-icon.priority-medium {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.recommendation-icon.priority-low {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.recommendation-text {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.recommendation-text strong {
color: var(--text-primary);
}
/* Loading Overlay */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 9999;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing-lg);
}
.loading-overlay.active {
display: flex;
}
.loading-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 450px;
width: 90%;
text-align: center;
}
.loading-header {
margin-bottom: var(--spacing-lg);
}
.loading-header h3 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.loading-header p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.loading-steps {
text-align: left;
margin-bottom: var(--spacing-lg);
}
.loading-step {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border-light, #f1f5f9);
}
.loading-step:last-child {
border-bottom: none;
}
.step-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-icon.pending { color: var(--text-tertiary); }
.step-icon.in_progress { color: #a855f7; }
.step-icon.complete { color: var(--success); }
.step-icon.error { color: var(--error); }
.step-icon.missing { color: var(--text-tertiary); opacity: 0.6; }
.step-icon.skipped { color: var(--text-tertiary); opacity: 0.5; }
.step-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: #a855f7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.step-text {
flex: 1;
font-size: var(--font-size-sm);
}
.step-text.pending { color: var(--text-tertiary); }
.step-text.in_progress { color: var(--text-primary); font-weight: 500; }
.step-text.complete { color: var(--text-secondary); }
.step-text.error { color: var(--error); }
.step-text.missing { color: var(--text-tertiary); font-style: italic; }
.step-text.skipped { color: var(--text-tertiary); opacity: 0.6; }
/* Source tag styling */
.source-tag {
display: inline-block;
font-size: 9px;
font-weight: 600;
padding: 1px 5px;
border-radius: 3px;
margin-left: 4px;
text-transform: uppercase;
vertical-align: middle;
}
.source-tag.website { background: #6366f1; color: white; }
.source-tag.brave { background: #fb542b; color: white; }
.source-tag.google { background: #4285f4; color: white; }
.source-tag.facebook { background: #1877f2; color: white; }
.source-tag.instagram { background: #e4405f; color: white; }
.source-tag.linkedin { background: #0a66c2; color: white; }
.source-tag.youtube { background: #ff0000; color: white; }
.source-tag.twitter { background: #000000; color: white; }
.source-tag.tiktok { background: #000000; color: white; }
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 400px;
width: 90%;
text-align: center;
}
.modal-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--spacing-md);
}
.modal-icon.success {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.modal-icon.error {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.modal-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-lg);
}
.modal-btn {
background: #a855f7;
color: white;
border: none;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
.modal-btn:hover {
background: #9333ea;
}
</style>
{% endblock %}
{% block content %}
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="{{ url_for('index') }}">Firmy</a>
<span class="breadcrumb-separator">/</span>
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
<span class="breadcrumb-separator">/</span>
<span>Audyt Social Media</span>
</div>
<div class="audit-header">
<div class="audit-header-info">
<h1>Audyt Social Media</h1>
<p>{{ company.name }}</p>
<div class="data-source-info">
<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="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
</svg>
<span>Analiza obecnosci w mediach spolecznosciowych</span>
</div>
</div>
<div class="header-actions">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn btn-outline btn-sm">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Profil firmy
</a>
{% if can_audit %}
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Uruchom audyt
</button>
{% endif %}
</div>
</div>
<!-- Score Section -->
{# Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green #}
{% set score = social_data.score %}
<div class="score-section">
<div class="score-circle" style="--score-percent: {{ score }}; --score-color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
<span class="score-value" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">{{ score }}</span>
<span class="score-label">/ 100</span>
</div>
<div class="score-details">
<div class="score-category" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
{% if score >= 90 %}
Doskonala obecnosc w Social Media
{% elif score >= 70 %}
Dobra obecnosc w Social Media
{% elif score >= 50 %}
Przecietna obecnosc w Social Media
{% elif score >= 30 %}
Obecnosc wymaga rozbudowy
{% else %}
Slaba obecnosc w Social Media
{% endif %}
</div>
<p class="score-description">
{% if score >= 80 %}
Firma jest obecna na wiekszosci waznych platform spolecznosciowych. Utrzymuj aktywnosc i rozwijaj zaangazowanie.
{% elif score >= 60 %}
Firma ma dobra obecnosc w social media. Rozważ dodanie brakujacych platform dla pelniejszego zasiegu.
{% elif score >= 40 %}
Firma jest obecna na kilku platformach. Warto rozszerzyc obecnosc o kolejne kanaly komunikacji.
{% else %}
Firma ma ograniczona obecnosc w social media. Zalecamy utworzenie profili na kluczowych platformach.
{% endif %}
</p>
<div class="platforms-summary">
<div class="platforms-summary-item found">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span>{{ social_data.platforms_count }} {{ 'platforma' if social_data.platforms_count == 1 else ('platformy' if social_data.platforms_count in [2,3,4] else 'platform') }}</span>
</div>
<div class="platforms-summary-item missing">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span>{{ social_data.total_platforms - social_data.platforms_count }} brakujacych</span>
</div>
</div>
</div>
</div>
<!-- Platforms Grid -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
</svg>
Platformy Social Media
</h2>
<div class="platforms-grid">
{% set platform_names = {'facebook': 'Facebook', 'instagram': 'Instagram', 'linkedin': 'LinkedIn', 'youtube': 'YouTube', 'twitter': 'X (Twitter)', 'tiktok': 'TikTok'} %}
{% set platform_icons = {'facebook': 'f', 'instagram': 'ig', 'linkedin': 'in', 'youtube': 'yt', 'twitter': 'X', 'tiktok': 'tk'} %}
{% for platform in social_data.all_platforms %}
{% set profile = social_data.profiles.get(platform) %}
<div class="platform-card {{ 'found' if profile else 'missing' }}">
<div class="platform-header">
<div class="platform-icon {{ platform }}">
{% if platform == '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>
{% elif platform == '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-.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"/></svg>
{% elif platform == '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.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"/></svg>
{% elif platform == 'youtube' %}
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>
{% elif platform == '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>
{% elif platform == '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>
{% endif %}
</div>
<span class="platform-name">{{ platform_names.get(platform, platform|title) }}</span>
{% if profile and profile.check_status == 'needs_verification' %}
<span class="platform-status needs-verification">
<svg width="12" height="12" 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>
Do weryfikacji
</span>
{% elif profile and profile.last_post_date and (now - profile.last_post_date).days < 90 %}
<span class="platform-status active">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Aktywny
</span>
{% elif profile %}
<span class="platform-status" style="background: var(--bg-secondary); color: var(--text-secondary);">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Znaleziony
</span>
{% else %}
<span class="platform-status inactive">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
Brak profilu
</span>
{% endif %}
</div>
<div class="platform-details">
{% if profile %}
<div class="platform-url">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
<a href="{{ profile.url }}" target="_blank" rel="noopener">{{ profile.url|truncate(40) }}</a>
</div>
{% if profile.check_status == 'needs_verification' %}
<div style="margin: 6px 0; padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 12px; color: #92400e;">
<strong>Do weryfikacji</strong> — link do profilu wymaga recznego sprawdzenia. Profil moze byc nieaktywny lub wskazywac na niewlasciwe konto.
</div>
{% endif %}
{% if platform == 'facebook' and 'profile.php?id=' in (profile.url or '') %}
<div style="margin: 6px 0; padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 12px; color: #92400e;">
<strong>Adres profilu zawiera numeryczne ID</strong> zamiast nazwy firmy. Zalecamy ustawienie niestandardowej nazwy uzytkownika (np. facebook.com/NazwaFirmy) w ustawieniach strony na Facebooku.
</div>
{% endif %}
<div class="platform-meta">
{% if profile.page_name %}
<div class="platform-meta-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{{ profile.page_name }}
</div>
{% endif %}
{% if profile.followers_count %}
<div class="platform-meta-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
{{ '{:,}'.format(profile.followers_count).replace(',', ' ') }} obserwujacych
</div>
{% endif %}
{% if profile.verified_at %}
<div class="platform-meta-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ profile.verified_at.strftime('%d.%m.%Y') }}
</div>
{% endif %}
{% if profile.has_bio %}
<div class="platform-meta-item" title="Ma opis profilu">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
</svg>
Opis
</div>
{% endif %}
{% if profile.has_profile_photo %}
<div class="platform-meta-item" title="Ma zdjecie profilowe">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Avatar
</div>
{% endif %}
{% if profile.has_cover_photo %}
<div class="platform-meta-item" title="Ma zdjecie w tle">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Cover
</div>
{% endif %}
{% if profile.posts_count_30d is not none and profile.posts_count_30d > 0 %}
<div class="platform-meta-item" title="Posty w ostatnich 30 dniach">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2"/>
</svg>
{{ profile.posts_count_30d }} postow/30d
</div>
{% endif %}
{% if profile.engagement_rate is not none %}
<div class="platform-meta-item" title="Wskaznik zaangazowania">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
{{ '%.1f'|format(profile.engagement_rate) }}% engage
</div>
{% endif %}
{% if profile.posts_count_365d is not none and profile.posts_count_365d > 0 %}
<div class="platform-meta-item" title="Posty w ostatnim roku">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ profile.posts_count_365d }} postow/rok
</div>
{% endif %}
{% if profile.last_post_date %}
<div class="platform-meta-item" title="Ostatni post">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Ostatni: {{ profile.last_post_date.strftime('%d.%m.%Y') }}
</div>
{% endif %}
{% if profile.posting_frequency_score is not none %}
<div class="platform-meta-item" title="Czestosc postowania: {{ profile.posting_frequency_score }}/10">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span style="padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; background: {% if profile.posting_frequency_score >= 7 %}#dcfce7; color: #166534{% elif profile.posting_frequency_score >= 4 %}#fef3c7; color: #92400e{% else %}#fee2e2; color: #991b1b{% endif %};">{{ profile.posting_frequency_score }}/10</span>
</div>
{% endif %}
{% if profile.content_types and profile.content_types is mapping %}
{% for ctype, ccount in profile.content_types.items() %}
<div class="platform-meta-item" title="{{ ctype|capitalize }}: {{ ccount }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if ctype == 'videos' or ctype == 'video' %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
{% else %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
{% endif %}
</svg>
{{ ctype|capitalize }}: {{ ccount }}
</div>
{% endfor %}
{% endif %}
{% if profile.profile_description %}
<div class="platform-meta-item" title="{{ profile.profile_description }}" style="max-width: 200px;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ profile.profile_description[:80] }}{% if profile.profile_description|length > 80 %}...{% endif %}</span>
</div>
{% endif %}
</div>
{% if profile and profile.profile_completeness_score is not none %}
<div style="margin-top: var(--spacing-sm);">
<div style="display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 4px;">
<span>Kompletnosc profilu</span>
<span>{{ profile.profile_completeness_score }}%</span>
</div>
<div style="height: 6px; background: var(--border); border-radius: 3px; overflow: hidden;">
<div style="height: 100%; width: {{ profile.profile_completeness_score }}%; border-radius: 3px; background: {% if profile.profile_completeness_score >= 80 %}#10b981{% elif profile.profile_completeness_score >= 50 %}#f59e0b{% else %}#ef4444{% endif %};"></div>
</div>
</div>
{% endif %}
{% else %}
<p class="platform-missing-text">Nie znaleziono profilu na tej platformie</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% if social_data.total_platforms - social_data.platforms_count > 0 %}
<!-- Recommendations -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Rekomendacje
</h2>
<div class="recommendations">
{% set missing_platforms = [] %}
{% for platform in social_data.all_platforms %}
{% if platform not in social_data.profiles %}
{% set _ = missing_platforms.append(platform) %}
{% endif %}
{% endfor %}
{% if 'facebook' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-high">!</div>
<div class="recommendation-text">
<strong>Facebook</strong> - Najpopularniejsza platforma w Polsce. Zalozenie strony firmowej pozwoli dotrzec do szerokiego grona klientow.
</div>
</div>
{% endif %}
{% if 'linkedin' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-high">!</div>
<div class="recommendation-text">
<strong>LinkedIn</strong> - Kluczowa platforma dla firm B2B. Idealna do budowania relacji biznesowych i employer brandingu.
</div>
</div>
{% endif %}
{% if 'instagram' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-medium">!</div>
<div class="recommendation-text">
<strong>Instagram</strong> - Swietna platforma do prezentacji wizualnej firmy. Szczegolnie wazna dla firm z produktami/uslugami wizualnymi.
</div>
</div>
{% endif %}
{% if 'youtube' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-medium">!</div>
<div class="recommendation-text">
<strong>YouTube</strong> - Druga najwieksza wyszukiwarka na swiecie. Video content buduje zaufanie i pokazuje ekspertyze.
</div>
</div>
{% endif %}
{% if 'tiktok' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-low">!</div>
<div class="recommendation-text">
<strong>TikTok</strong> - Najszybciej rosnaca platforma. Warto rozwazyc jesli celujecie w mlodszych odbiorców.
</div>
</div>
{% endif %}
{% if 'twitter' in missing_platforms %}
<div class="recommendation-item">
<div class="recommendation-icon priority-low">!</div>
<div class="recommendation-text">
<strong>X (Twitter)</strong> - Platforma do szybkiej komunikacji i budowania wizerunku eksperta. Przydatna w branzy tech/media.
</div>
</div>
{% endif %}
</div>
{% endif %}
{# Facebook numeric ID warning - shown regardless of missing platforms #}
{% set fb_profile = social_data.profiles.get('facebook') %}
{% if fb_profile and 'profile.php?id=' in (fb_profile.url or '') %}
{% if social_data.total_platforms - social_data.platforms_count <= 0 %}
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Rekomendacje
</h2>
{% endif %}
<div class="recommendations">
<div class="recommendation-item">
<div class="recommendation-icon priority-high">!</div>
<div class="recommendation-text">
<strong>Facebook - zmien adres profilu</strong> - Twoj profil na Facebooku uzywa numerycznego ID zamiast nazwy firmy. Klienci latwiej zapamietaja adres typu <em>facebook.com/NazwaFirmy</em>. Aby to zmienic: Ustawienia strony na Facebooku &rarr; Ogolne &rarr; Nazwa uzytkownika &rarr; wpisz nazwe firmy.
</div>
</div>
</div>
{% endif %}
{% if social_data %}
{% with audit_type='social' %}
{% include 'partials/audit_ai_actions.html' %}
{% endwith %}
{% endif %}
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<div class="loading-header">
<h3>Audyt Social Media</h3>
<p>Szukam profili w mediach spolecznosciowych...</p>
</div>
<div class="loading-steps" id="loadingSteps">
<!-- Step 1: Scan Website -->
<div class="loading-step" id="step-website">
<div class="step-icon in_progress">
<div class="step-spinner"></div>
</div>
<span class="step-text in_progress">Skanuje strone WWW <span class="source-tag website">WWW</span></span>
</div>
<!-- Step 2: Facebook -->
<div class="loading-step" id="step-facebook">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam Facebook <span class="source-tag facebook">FB</span></span>
</div>
<!-- Step 3: Instagram -->
<div class="loading-step" id="step-instagram">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam Instagram <span class="source-tag instagram">IG</span></span>
</div>
<!-- Step 4: LinkedIn -->
<div class="loading-step" id="step-linkedin">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam LinkedIn <span class="source-tag linkedin">LI</span></span>
</div>
<!-- Step 5: YouTube -->
<div class="loading-step" id="step-youtube">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam YouTube <span class="source-tag youtube">YT</span></span>
</div>
<!-- Step 6: Twitter/X -->
<div class="loading-step" id="step-twitter">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam X (Twitter) <span class="source-tag twitter">X</span></span>
</div>
<!-- Step 7: TikTok -->
<div class="loading-step" id="step-tiktok">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Szukam TikTok <span class="source-tag tiktok">TK</span></span>
</div>
<!-- Step 8: Google Business -->
<div class="loading-step" id="step-google">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Pobieram dane Google <span class="source-tag google">Google</span></span>
</div>
<!-- Step 9: Save -->
<div class="loading-step" id="step-save">
<div class="step-icon pending">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke-width="2"/>
</svg>
</div>
<span class="step-text pending">Zapisuje wyniki</span>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal-content">
<div class="modal-icon" id="modalIcon">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="modal-title" id="modalTitle">Sukces</div>
<div class="modal-description" id="modalDescription">Operacja zakonczona pomyslnie.</div>
<button class="modal-btn" onclick="closeModal()">OK</button>
</div>
</div>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
const companySlug = '{{ company.slug }}';
// All step IDs in order
const allSteps = [
'step-website',
'step-facebook', 'step-instagram', 'step-linkedin',
'step-youtube', 'step-twitter', 'step-tiktok',
'step-google', 'step-save'
];
// Platform step mapping
const platformSteps = {
'facebook': 'step-facebook',
'instagram': 'step-instagram',
'linkedin': 'step-linkedin',
'youtube': 'step-youtube',
'twitter': 'step-twitter',
'tiktok': 'step-tiktok'
};
// SVG icons for different states
const icons = {
pending: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg>',
in_progress: '<div class="step-spinner"></div>',
complete: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
error: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
missing: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/></svg>'
};
function resetSteps() {
allSteps.forEach((stepId, index) => {
const stepEl = document.getElementById(stepId);
if (stepEl) {
const iconEl = stepEl.querySelector('.step-icon');
const textEl = stepEl.querySelector('.step-text');
if (index === 0) {
iconEl.className = 'step-icon in_progress';
iconEl.innerHTML = icons.in_progress;
textEl.className = 'step-text in_progress';
} else {
iconEl.className = 'step-icon pending';
iconEl.innerHTML = icons.pending;
textEl.className = 'step-text pending';
}
}
});
}
function updateStep(stepId, status, message) {
const stepEl = document.getElementById(stepId);
if (!stepEl) return;
const iconEl = stepEl.querySelector('.step-icon');
const textEl = stepEl.querySelector('.step-text');
iconEl.className = 'step-icon ' + status;
iconEl.innerHTML = icons[status] || icons.pending;
textEl.className = 'step-text ' + status;
if (message) {
textEl.innerHTML = message;
}
}
function showLoading() {
resetSteps();
document.getElementById('loadingOverlay').classList.add('active');
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.remove('active');
}
function showModal(title, description, isSuccess) {
const modal = document.getElementById('modalOverlay');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const descEl = document.getElementById('modalDescription');
titleEl.textContent = title;
descEl.textContent = description;
icon.className = 'modal-icon ' + (isSuccess ? 'success' : 'error');
icon.innerHTML = isSuccess
? '<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>'
: '<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>';
modal.classList.add('active');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('active');
}
// Animate step-by-step progress for social media platforms
async function animatePlatformSteps(foundPlatforms, googleData) {
const delay = 400; // ms between steps - slower for readability
// Website scan complete
updateStep('step-website', 'complete', 'Skanowanie strony WWW zakonczone <span class="source-tag website">WWW</span>');
await new Promise(r => setTimeout(r, delay));
// Process each platform
const platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok'];
for (const platform of platforms) {
const stepId = platformSteps[platform];
const platformLabel = platform === 'twitter' ? 'X (Twitter)' : platform.charAt(0).toUpperCase() + platform.slice(1);
const sourceTag = `<span class="source-tag ${platform}">${platform === 'twitter' ? 'X' : platform.substring(0,2).toUpperCase()}</span>`;
updateStep(stepId, 'in_progress', `Szukam ${platformLabel}... ${sourceTag}`);
await new Promise(r => setTimeout(r, delay / 2));
if (foundPlatforms && foundPlatforms.includes(platform)) {
updateStep(stepId, 'complete', `<strong>${platformLabel}</strong>: ZNALEZIONO ${sourceTag}`);
} else {
updateStep(stepId, 'missing', `${platformLabel}: brak profilu ${sourceTag}`);
}
await new Promise(r => setTimeout(r, delay / 2));
}
// Google data
updateStep('step-google', 'in_progress', 'Pobieram dane z Google... <span class="source-tag google">Google</span>');
await new Promise(r => setTimeout(r, delay));
if (googleData && (googleData.google_rating || googleData.google_reviews_count)) {
const rating = googleData.google_rating ? `${googleData.google_rating}/5` : '';
const reviews = googleData.google_reviews_count ? `${googleData.google_reviews_count} opinii` : '';
const details = [rating, reviews].filter(Boolean).join(', ');
updateStep('step-google', 'complete', `Google: ${details} <span class="source-tag google">Google</span>`);
} else {
updateStep('step-google', 'missing', 'Google: brak profilu GBP <span class="source-tag google">Google</span>');
}
}
async function runAudit() {
const btn = document.getElementById('runAuditBtn');
if (btn) {
btn.disabled = true;
}
showLoading();
// Start animation
await new Promise(r => setTimeout(r, 300));
try {
const response = await fetch('/api/social/audit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ slug: companySlug })
});
const data = await response.json();
console.log('Audit response:', data); // Debug
// Animate the steps with found platforms and Google data
const foundPlatforms = data.platforms || [];
const googleData = data.google_reviews || {};
await animatePlatformSteps(foundPlatforms, googleData);
// Save step
updateStep('step-save', 'in_progress', 'Zapisuje wyniki do bazy...');
await new Promise(r => setTimeout(r, 400));
if (response.ok && data.success) {
const summary = `Zapisano: ${data.profiles_found || 0} profili, wynik: ${data.score || 0}/100`;
updateStep('step-save', 'complete', summary);
// Wait longer for user to see complete results
await new Promise(r => setTimeout(r, 4000));
hideLoading();
showModal('Audyt zakonczony', `Znaleziono ${data.profiles_found || 0} profili social media. Strona zostanie odswiezona.`, true);
setTimeout(() => location.reload(), 2000);
} else {
updateStep('step-save', 'error', 'Blad zapisu: ' + (data.error || 'nieznany blad'));
await new Promise(r => setTimeout(r, 4000));
hideLoading();
showModal('Blad', data.error || 'Wystapil nieznany blad podczas audytu.', false);
if (btn) btn.disabled = false;
}
} catch (error) {
hideLoading();
showModal('Blad polaczenia', 'Nie udalo sie polaczyc z serwerem: ' + error.message, false);
if (btn) btn.disabled = false;
}
}
// Close modal on overlay click
document.getElementById('modalOverlay').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
/* ============================================================
AI AUDIT ACTIONS
============================================================ */
const companyId = {{ company.id }};
const auditType = 'social';
function simpleMarkdown(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
.replace(/\n/g, '<br>');
}
async function runAIAnalysis(force) {
const prompt = document.getElementById('aiAnalyzePrompt');
const loading = document.getElementById('aiLoading');
const results = document.getElementById('aiResults');
const btn = document.getElementById('aiAnalyzeBtn');
if (btn) btn.disabled = true;
if (prompt) prompt.style.display = 'none';
if (results) results.style.display = 'none';
if (loading) loading.style.display = 'block';
let seconds = 0;
const timerEl = document.getElementById('aiTimer');
const timerInterval = setInterval(() => {
seconds++;
if (timerEl) timerEl.textContent = seconds + 's';
}, 1000);
try {
const response = await fetch('/api/audit/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
company_id: companyId,
audit_type: auditType,
force: !!force
})
});
const data = await response.json();
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (data.success) {
renderAIResults(data);
} else {
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad analizy AI</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany blad')}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
</div>`;
results.style.display = 'block';
}
} catch (error) {
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad polaczenia</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
</div>`;
results.style.display = 'block';
}
}
function renderAIResults(data) {
const results = document.getElementById('aiResults');
const summaryEl = document.getElementById('aiSummaryText');
const cacheInfo = document.getElementById('aiCacheInfo');
const actionsList = document.getElementById('aiActionsList');
summaryEl.textContent = data.summary || '';
if (data.cached && data.generated_at) {
const d = new Date(data.generated_at);
document.getElementById('aiCacheDate').textContent =
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
cacheInfo.style.display = 'block';
} else {
cacheInfo.style.display = 'none';
}
actionsList.innerHTML = '';
const actions = data.actions || [];
const priorityLabels = {critical: 'KRYTYCZNE', high: 'WYSOKI', medium: 'SREDNI', low: 'NISKI'};
actions.forEach((action, idx) => {
const impact = action.impact_score || 5;
const effort = action.effort_score || 5;
const card = document.createElement('div');
card.className = 'ai-action-card priority-' + (action.priority || 'medium');
card.id = 'ai-action-' + idx;
card.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-sm); flex-wrap: wrap; gap: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<span class="ai-priority-badge ${action.priority || 'medium'}">${priorityLabels[action.priority] || 'SREDNI'}</span>
<span class="ai-action-title" style="font-weight: 600; color: var(--text-primary);">${escapeHtml(action.title || '')}</span>
</div>
</div>
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm);">${escapeHtml(action.description || '')}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin-bottom: var(--spacing-sm);">
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wplyw: ${impact}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill impact" style="width: ${impact * 10}%;"></div></div>
</div>
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wysilek: ${effort}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill effort" style="width: ${effort * 10}%;"></div></div>
</div>
</div>
<div class="ai-action-buttons">
<button class="btn btn-outline btn-sm" onclick="generateContent('${action.action_type}', ${idx})">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
Wygeneruj tresc
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'implemented')" style="color: #10b981; border-color: #10b981;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Zrobione
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'dismissed')" style="color: var(--text-tertiary); border-color: var(--border);">Odrzuc</button>
</div>
<div id="ai-content-${idx}" style="display: none;"></div>
`;
actionsList.appendChild(card);
});
// Render comparison with previous analysis if available
if (typeof renderAIComparison === 'function') renderAIComparison(data);
results.style.display = 'block';
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
window._aiActions = actions;
}
async function generateContent(actionType, idx) {
const container = document.getElementById('ai-content-' + idx);
if (!container) return;
if (container.dataset.loaded === 'true') {
container.style.display = container.style.display === 'none' ? 'block' : 'none';
return;
}
container.innerHTML = '<div style="padding: var(--spacing-md); color: var(--text-secondary); font-size: var(--font-size-sm);">Generowanie tresci...</div>';
container.style.display = 'block';
try {
const response = await fetch('/api/audit/generate-content', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken},
body: JSON.stringify({company_id: companyId, action_type: actionType, context: {}})
});
const data = await response.json();
if (data.success && data.content) {
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
if (isCode) {
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
} else {
container.innerHTML = `
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
</div>`;
}
container.dataset.loaded = 'true';
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
} else {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(data.error || 'Blad generowania')}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
} catch (error) {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
}
function copyContent(btn) {
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
if (!code) return;
navigator.clipboard.writeText(code.textContent).then(() => {
const orig = btn.textContent;
btn.textContent = 'Skopiowano!';
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
function markAction(idx, status) {
const card = document.getElementById('ai-action-' + idx);
if (!card) return;
if (status === 'implemented') card.classList.add('implemented');
else if (status === 'dismissed') card.classList.add('dismissed');
const actions = window._aiActions || [];
if (actions[idx] && actions[idx].id) {
fetch('/api/audit/actions/' + actions[idx].id + '/status', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken},
body: JSON.stringify({status: status})
}).catch(() => {});
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
{% endblock %}