nordabiz/templates/admin/social_audit_dashboard.html
Maciej Pienczyn 1612deeb53
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
refactor: remove /admin/social-media panel, rename menu labels
Remove unused social-media analytics panel (replaced by social-audit).
Rename admin menu items for clarity:
- Forum → Moderacja forum
- Ogłoszenia → Moderacja ogłoszeń
- Rekomendacje → Moderacja rekomendacji
- Deklaracje → Zarządzanie deklaracjami
- Składki → Zarządzanie składkami
- Korzyści → Zarządzanie korzyściami
- Social Media → Audyt social media
- Social Dashboard → Publikacja social media
- Kalendarz → Zarządzanie kalendarzem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:29:19 +01:00

1207 lines
56 KiB
HTML

{% extends "base.html" %}
{% block title %}Panel Audyt Social Media - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.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: var(--info-light, #e0f2fe);
border-radius: var(--radius);
font-size: var(--font-size-sm);
color: var(--info, #0284c7);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
/* Summary Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background: white;
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
text-align: center;
}
.stat-number {
font-size: var(--font-size-2xl);
font-weight: 700;
display: block;
margin-bottom: var(--spacing-xs);
}
.stat-number.green { color: var(--success); }
.stat-number.yellow { color: var(--warning); }
.stat-number.red { color: var(--error); }
.stat-number.gray { color: var(--secondary); }
.stat-number.blue { color: var(--primary); }
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
/* Section Title */
.section-title {
font-size: var(--font-size-xl);
font-weight: 600;
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
/* Platform Coverage Grid */
.platform-coverage-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.platform-card {
background: white;
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.platform-icon {
width: 48px;
height: 48px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.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-info {
flex: 1;
}
.platform-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.platform-stats {
display: flex;
gap: var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.platform-stats strong {
color: var(--text-primary);
}
.platform-progress {
width: 100px;
text-align: right;
}
.platform-percent {
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--text-primary);
}
.progress-bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-top: var(--spacing-xs);
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-fill.high { background: var(--success); }
.progress-fill.medium { background: var(--warning); }
.progress-fill.low { background: var(--error); }
/* Top Followers Section */
.top-followers {
background: white;
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: var(--spacing-xl);
}
.top-followers-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
.top-follower-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--background);
border-radius: var(--radius);
}
.top-rank {
width: 24px;
height: 24px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-sm);
font-weight: 700;
flex-shrink: 0;
}
.top-company-info {
flex: 1;
min-width: 0;
}
.top-company-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.top-company-name a {
color: var(--text-primary);
text-decoration: none;
}
.top-company-name a:hover {
color: var(--primary);
}
.top-followers-count {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
/* Filters */
.filters-bar {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
align-items: center;
background: white;
padding: var(--spacing-md);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.filter-group {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.filter-group label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-weight: 500;
}
.filter-group select,
.filter-group input {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-sm);
min-width: 150px;
}
/* Table */
.table-container {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow-x: auto;
}
.social-table {
width: 100%;
min-width: 900px;
border-collapse: collapse;
font-size: var(--font-size-sm);
}
.social-table th,
.social-table td {
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.social-table th {
background: var(--background);
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.social-table th:hover {
background: #e9ecef;
}
.social-table th .sort-icon {
display: inline-block;
margin-left: var(--spacing-xs);
opacity: 0.3;
}
.social-table th.sorted .sort-icon {
opacity: 1;
}
.social-table th.sorted-asc .sort-icon::after {
content: '\2191';
}
.social-table th.sorted-desc .sort-icon::after {
content: '\2193';
}
.social-table tbody tr:hover {
background: var(--background);
}
.company-name-cell {
font-weight: 500;
max-width: 250px;
}
.company-name-cell a {
color: var(--text-primary);
text-decoration: none;
}
.company-name-cell a:hover {
color: var(--primary);
}
/* Platform icons in table */
.platform-icons {
display: grid;
grid-template-columns: repeat(3, 20px);
gap: 2px;
}
.platform-icon-small {
width: 20px;
height: 20px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
}
.platform-icon-small.active {
color: white;
}
.platform-icon-small.inactive {
background: var(--border);
color: var(--text-secondary);
opacity: 0.3;
}
.platform-icon-small.facebook.active { background: #1877f2; }
.platform-icon-small.instagram.active { background: linear-gradient(45deg, #f09433, #e6683c, #dc2743); }
.platform-icon-small.linkedin.active { background: #0a66c2; }
.platform-icon-small.youtube.active { background: #ff0000; }
.platform-icon-small.twitter.active { background: #000000; }
.platform-icon-small.tiktok.active { background: #000000; }
/* Category badge */
.category-badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
background: var(--border);
color: var(--text-secondary);
}
/* Date cell */
.date-cell {
font-size: var(--font-size-sm);
color: var(--text-secondary);
white-space: nowrap;
}
.date-old {
color: var(--warning);
}
.date-never {
color: var(--error);
font-style: italic;
}
/* Action buttons */
.action-buttons {
display: flex;
gap: var(--spacing-xs);
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: var(--transition);
text-decoration: none;
color: var(--text-primary);
}
.btn-icon:hover {
background: var(--background);
border-color: var(--primary);
color: var(--primary);
}
/* Activity indicator */
.activity-dot {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--font-size-sm);
white-space: nowrap;
}
.activity-dot::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.activity-dot.active::before { background: #22c55e; }
.activity-dot.warning::before { background: #f59e0b; }
.activity-dot.inactive::before { background: #ef4444; }
.activity-dot.unknown::before { background: #d1d5db; }
/* Completeness bar (mini) */
.completeness-bar {
display: flex;
align-items: center;
gap: 6px;
}
.completeness-track {
width: 60px;
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.completeness-fill {
height: 100%;
border-radius: 3px;
}
.completeness-fill.high { background: #22c55e; }
.completeness-fill.medium { background: #f59e0b; }
.completeness-fill.low { background: #ef4444; }
.completeness-value {
font-size: 12px;
font-weight: 500;
min-width: 30px;
}
/* Empty state */
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 1200px) {
.social-table {
font-size: var(--font-size-sm);
}
.hide-mobile {
display: none;
}
}
@media (max-width: 768px) {
.filters-bar {
flex-direction: column;
align-items: stretch;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.platform-coverage-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="admin-header">
<div>
<h1>Panel Audyt Social Media</h1>
<p class="text-muted">Analiza obecności w mediach społecznościowych członków Norda Biznes</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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Dane z: Facebook, Instagram, LinkedIn, YouTube, Twitter/X, TikTok</span>
</div>
</div>
<div class="header-actions">
<button id="enrichBtn" class="btn btn-primary btn-sm" onclick="startEnrichment()" title="Uruchom scraping publicznych profili social media dla wszystkich firm">
<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>
<span id="enrichMiniStatus" style="display: none; font-size: 12px; color: #2563eb; font-weight: 500;"></span>
</div>
</div>
<!-- Live enrichment panel -->
<div id="enrichPanel" style="display: none; margin-bottom: var(--spacing-xl); background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden;">
<div style="padding: var(--spacing-md) var(--spacing-lg); background: #eff6ff; border-bottom: 1px solid #bfdbfe; display: flex; align-items: center; gap: var(--spacing-md);">
<div id="enrichSpinner" style="width: 20px; height: 20px; border: 3px solid #bfdbfe; border-top-color: #2563eb; border-radius: 50%; animation: spin 0.8s linear infinite;"></div>
<div style="flex: 1;">
<div style="font-weight: 600; color: #1e40af;" id="enrichTitle">Skanowanie profili social media...</div>
<div style="font-size: var(--font-size-xs); color: #3b82f6;" id="enrichSubtitle">Dane nie zostaną zapisane bez Twojej zgody</div>
</div>
<span id="enrichCounter" style="font-size: var(--font-size-sm); font-weight: 600; color: #1e40af;">0 / 0</span>
</div>
<!-- Progress bar -->
<div style="height: 4px; background: #dbeafe;">
<div id="enrichBar" style="height: 100%; background: #2563eb; transition: width 0.5s; width: 0%;"></div>
</div>
<!-- Live feed -->
<div id="enrichFeed" style="max-height: 400px; overflow-y: auto; padding: var(--spacing-sm) var(--spacing-lg); font-size: var(--font-size-sm);"></div>
</div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
<!-- Confirm modal -->
<div id="enrichConfirm" style="display:none; position:fixed; inset:0; z-index:1000; background:rgba(0,0,0,0.4); display:none; align-items:center; justify-content:center;">
<div style="background:white; border-radius:var(--radius-lg); box-shadow:0 20px 60px rgba(0,0,0,0.3); max-width:480px; width:90%; padding:var(--spacing-xl);">
<div style="display:flex; align-items:center; gap:var(--spacing-md); margin-bottom:var(--spacing-lg);">
<div style="width:48px; height:48px; border-radius:50%; background:#eff6ff; display:flex; align-items:center; justify-content:center; flex-shrink:0;">
<svg width="24" height="24" fill="none" stroke="#2563eb" 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>
</div>
<div>
<h3 style="margin:0; font-size:var(--font-size-lg);">Uruchomić skanowanie?</h3>
<p style="margin:4px 0 0; font-size:var(--font-size-sm); color:var(--text-secondary);">Audyt social media dla wszystkich firm</p>
</div>
</div>
<!-- Platform filter checkboxes -->
<div style="margin-bottom:var(--spacing-lg);">
<div style="font-size:var(--font-size-sm); font-weight:600; margin-bottom:8px;">Platformy do skanowania:</div>
<div style="display:flex; gap:var(--spacing-sm); flex-wrap:wrap;">
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="facebook" checked> Facebook
</label>
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="instagram" checked> Instagram
</label>
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="linkedin" checked> LinkedIn
</label>
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="youtube" checked> YouTube
</label>
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="twitter" checked> Twitter/X
</label>
<label style="display:flex; align-items:center; gap:4px; font-size:var(--font-size-sm); cursor:pointer; padding:4px 10px; border-radius:var(--radius); border:1px solid var(--border-color, #e5e7eb); background:var(--background);">
<input type="checkbox" class="enrich-platform" value="tiktok" checked> TikTok
</label>
</div>
</div>
<div style="font-size:var(--font-size-sm); color:var(--text-secondary); margin-bottom:var(--spacing-lg);">
<div style="display:flex; align-items:flex-start; gap:8px; margin-bottom:8px;">
<svg width="16" height="16" fill="none" stroke="#22c55e" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
<span>Dane <strong>nie zostaną zapisane</strong> bez Twojej zgody — najpierw zobaczysz raport ze zmianami</span>
</div>
<div style="display:flex; align-items:flex-start; gap:8px; margin-bottom:8px;">
<svg width="16" height="16" fill="none" stroke="#3b82f6" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><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>
<span>Profile z danymi z OAuth API nie będą nadpisywane</span>
</div>
<div style="display:flex; align-items:flex-start; gap:8px;">
<svg width="16" height="16" fill="none" stroke="#f59e0b" viewBox="0 0 24 24" style="flex-shrink:0; margin-top:2px;"><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>
<span>Proces działa w tle — możesz obserwować postęp na bieżąco</span>
</div>
</div>
<div style="display:flex; gap:var(--spacing-sm); justify-content:flex-end;">
<button class="btn btn-outline btn-sm" onclick="document.getElementById('enrichConfirm').style.display='none'">Anuluj</button>
<button class="btn btn-primary btn-sm" onclick="document.getElementById('enrichConfirm').style.display='none'; doStartEnrichment();">
<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="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 skanowanie
</button>
</div>
</div>
</div>
<!-- Summary Stats -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-number">{{ stats.total_companies }}</span>
<span class="stat-label">Wszystkich firm</span>
</div>
<div class="stat-card">
<span class="stat-number green">{{ stats.companies_with_sm }}</span>
<span class="stat-label">Z Social Media</span>
</div>
<div class="stat-card">
<span class="stat-number red">{{ stats.companies_without_sm }}</span>
<span class="stat-label">Bez Social Media</span>
</div>
<div class="stat-card">
<span class="stat-number blue">{{ stats.total_profiles }}</span>
<span class="stat-label">Łącznie profili</span>
</div>
<div class="stat-card">
<span class="stat-number">{{ "{:,}".format(stats.total_followers).replace(",", " ") }}</span>
<span class="stat-label">Łącznie obserwujących</span>
</div>
<div class="stat-card">
<span class="stat-number {{ 'green' if stats.avg_completeness >= 60 else ('yellow' if stats.avg_completeness >= 30 else 'red') }}">{{ stats.avg_completeness }}%</span>
<span class="stat-label">Śr. kompletność profilu</span>
</div>
<div class="stat-card">
<span class="stat-number">{{ stats.avg_engagement }}%</span>
<span class="stat-label">Śr. engagement</span>
</div>
{% if stats.inactive_30d > 0 %}
<div class="stat-card" style="border-top: 4px solid var(--error);">
<span class="stat-number red">{{ stats.inactive_30d }}</span>
<span class="stat-label">Nieaktywne &gt;30 dni</span>
</div>
{% endif %}
{% if stats.needs_verification_count > 0 %}
<div class="stat-card" style="border-top: 4px solid #f59e0b;">
<span class="stat-number yellow">{{ stats.needs_verification_count }}</span>
<span class="stat-label">Do weryfikacji</span>
</div>
{% endif %}
</div>
{% if needs_verification and needs_verification|length > 0 %}
<!-- Needs Verification Section -->
<div style="background: #fffbeb; border: 1px solid #f59e0b; border-radius: var(--radius-lg); padding: var(--spacing-lg); margin-bottom: var(--spacing-xl);">
<h2 style="font-size: var(--font-size-lg); font-weight: 600; color: #92400e; margin-bottom: var(--spacing-md); display: flex; align-items: center; gap: var(--spacing-sm);">
<svg width="20" height="20" fill="none" stroke="#f59e0b" 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>
Profile do ręcznej weryfikacji
</h2>
<div style="display: grid; gap: var(--spacing-sm);">
{% for item in needs_verification %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md); background: white; border-radius: var(--radius); border-left: 3px solid #f59e0b;">
<span style="font-weight: 600; min-width: 80px; text-transform: capitalize;">{{ item.platform }}</span>
<a href="{{ url_for('company_detail', company_id=item.company_id) }}" style="color: var(--text-primary); font-weight: 500;">{{ item.company_name }}</a>
<a href="{{ item.url }}" target="_blank" rel="noopener" style="color: var(--primary); font-size: var(--font-size-sm); margin-left: auto;">{{ item.url|truncate(50) }}</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Platform Coverage Section -->
<h2 class="section-title">Pokrycie platform</h2>
<div class="platform-coverage-grid">
{% set platform_icons = {
'facebook': '<svg width="24" height="24" 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="24" height="24" 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="24" height="24" 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="24" height="24" 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="24" height="24" 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="24" height="24" 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 platform in platforms %}
<div class="platform-card">
<div class="platform-icon {{ platform }}">
{{ platform_icons[platform]|safe }}
</div>
<div class="platform-info">
<div class="platform-name">{{ platform|capitalize }}</div>
<div class="platform-stats">
<span><strong>{{ stats.platform_stats[platform].count }}</strong> firm</span>
</div>
</div>
<div class="platform-progress">
<div class="platform-percent">{{ stats.platform_stats[platform].percent }}%</div>
<div class="progress-bar">
<div class="progress-fill {{ 'high' if stats.platform_stats[platform].percent >= 40 else ('medium' if stats.platform_stats[platform].percent >= 20 else 'low') }}"
style="width: {{ stats.platform_stats[platform].percent }}%"></div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Top Followers Section -->
{% if top_followers %}
<div class="top-followers">
<h2 class="section-title">Top 10 — Najwięcej obserwujących</h2>
<div class="top-followers-list">
{% for company in top_followers %}
<div class="top-follower-item">
<div class="top-rank">{{ loop.index }}</div>
<div class="top-company-info">
<div class="top-company-name">
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
</div>
<div class="top-followers-count">{{ "{:,}".format(company.total_followers).replace(",", " ") }} obserwujących</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Filters -->
<div class="filters-bar">
<div class="filter-group">
<label for="filterCategory">Kategoria:</label>
<select id="filterCategory" onchange="applyFilters()">
<option value="">Wszystkie</option>
{% for category in categories %}
<option value="{{ category }}">{{ category }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label for="filterPlatform">Platforma:</label>
<select id="filterPlatform" onchange="applyFilters()">
<option value="">Wszystkie</option>
<option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="linkedin">LinkedIn</option>
<option value="youtube">YouTube</option>
<option value="twitter">Twitter/X</option>
<option value="tiktok">TikTok</option>
<option value="none">Bez profili</option>
</select>
</div>
<div class="filter-group">
<label for="filterActivity">Aktywność:</label>
<select id="filterActivity" onchange="applyFilters()">
<option value="">Wszystkie</option>
<option value="active">Aktywne (&le;7 dni)</option>
<option value="warning">Spadek (&le;30 dni)</option>
<option value="inactive">Nieaktywne (&gt;30 dni)</option>
<option value="unknown">Brak danych</option>
</select>
</div>
<div class="filter-group">
<label for="filterSearch">Szukaj:</label>
<input type="text" id="filterSearch" placeholder="Nazwa firmy..." oninput="applyFilters()">
</div>
<div class="filter-group" style="margin-left: auto;">
<button class="btn btn-sm btn-outline" onclick="resetFilters()">Resetuj filtry</button>
</div>
</div>
<!-- Table -->
{% if companies %}
<div class="table-container">
<table class="social-table" id="socialTable">
<thead>
<tr>
<th data-sort="name">
Firma <span class="sort-icon"></span>
</th>
<th data-sort="category" class="hide-mobile">
Kategoria <span class="sort-icon"></span>
</th>
<th data-sort="platforms" class="sorted sorted-desc">
Platformy <span class="sort-icon"></span>
</th>
<th data-sort="followers">
Obserwujący <span class="sort-icon"></span>
</th>
<th data-sort="activity" class="hide-mobile">
Aktywność <span class="sort-icon"></span>
</th>
<th data-sort="health" class="hide-mobile">
Ocena <span class="sort-icon"></span>
</th>
<th class="hide-mobile">Zalecenia</th>
<th data-sort="date" class="hide-mobile">
Weryfikacja <span class="sort-icon"></span>
</th>
<th>Akcje</th>
</tr>
</thead>
<tbody id="socialTableBody">
{% for company in companies %}
{% set activity_days = (now - company.last_post).days if company.last_post else 9999 %}
<tr data-category="{{ company.category }}"
data-name="{{ company.name|lower }}"
data-platforms="{{ company.platform_count }}"
data-followers="{{ company.total_followers }}"
data-activity="{{ activity_days }}"
data-completeness="{{ company.avg_completeness }}"
data-health="{{ company.health_score }}"
data-date="{{ company.last_verified.isoformat() if company.last_verified else '1970-01-01' }}"
data-has-facebook="{{ 'true' if company.has_facebook else 'false' }}"
data-has-instagram="{{ 'true' if company.has_instagram else 'false' }}"
data-has-linkedin="{{ 'true' if company.has_linkedin else 'false' }}"
data-has-youtube="{{ 'true' if company.has_youtube else 'false' }}"
data-has-twitter="{{ 'true' if company.has_twitter else 'false' }}"
data-has-tiktok="{{ 'true' if company.has_tiktok else 'false' }}">
<td class="company-name-cell">
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
</td>
<td class="hide-mobile">
<span class="category-badge">{{ company.category or 'Inne' }}</span>
</td>
<td>
<div class="platform-icons">
<div class="platform-icon-small facebook {{ 'active' if company.has_facebook else 'inactive' }}" title="Facebook">
<svg width="10" height="10" 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>
</div>
<div class="platform-icon-small instagram {{ 'active' if company.has_instagram else 'inactive' }}" title="Instagram">
<svg width="10" height="10" 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>
</div>
<div class="platform-icon-small linkedin {{ 'active' if company.has_linkedin else 'inactive' }}" title="LinkedIn">
<svg width="10" height="10" 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>
</div>
<div class="platform-icon-small youtube {{ 'active' if company.has_youtube else 'inactive' }}" title="YouTube">
<svg width="10" height="10" 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>
</div>
<div class="platform-icon-small twitter {{ 'active' if company.has_twitter else 'inactive' }}" title="Twitter/X">
<svg width="10" height="10" 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>
</div>
<div class="platform-icon-small tiktok {{ 'active' if company.has_tiktok else 'inactive' }}" title="TikTok">
<svg width="10" height="10" 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>
</div>
</div>
</td>
<td>
{% if company.total_followers > 0 %}
{{ "{:,}".format(company.total_followers).replace(",", " ") }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="hide-mobile">
{% if company.platform_count > 0 %}
{% if company.last_post %}
{% set days = (now - company.last_post).days %}
{% if days <= 7 %}
<span class="activity-dot active" title="Ostatni post: {{ company.last_post.strftime('%d.%m.%Y') }}">{{ days }}d</span>
{% elif days <= 30 %}
<span class="activity-dot warning" title="Ostatni post: {{ company.last_post.strftime('%d.%m.%Y') }}">{{ days }}d</span>
{% else %}
<span class="activity-dot inactive" title="Ostatni post: {{ company.last_post.strftime('%d.%m.%Y') }}">{{ days }}d</span>
{% endif %}
{% else %}
<span class="activity-dot unknown">b/d</span>
{% endif %}
{% if company.total_posts_30d > 0 %}
<span style="font-size: 11px; color: var(--text-secondary); margin-left: 2px;" title="Posty w ostatnich 30 dniach">({{ company.total_posts_30d }})</span>
{% endif %}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="hide-mobile">
{% if company.health_score > 0 or company.avg_completeness > 0 %}
<div title="Zdrowie: {{ company.health_score }}/100, Profil: {{ company.avg_completeness }}%">
<div class="completeness-bar" style="margin-bottom: 3px;">
<div class="completeness-track">
<div class="completeness-fill {{ 'high' if company.health_score >= 60 else ('medium' if company.health_score >= 30 else 'low') }}"
style="width: {{ company.health_score }}%"></div>
</div>
<span class="completeness-value" style="font-weight: 600; color: {{ '#22c55e' if company.health_score >= 60 else ('#f59e0b' if company.health_score >= 30 else '#ef4444') }};">{{ company.health_score }}</span>
</div>
{% if company.avg_completeness > 0 %}
<div style="font-size: 10px; color: var(--text-secondary);">profil {{ company.avg_completeness }}%</div>
{% endif %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="hide-mobile" style="font-size: 11px; white-space: nowrap;">
{% if company.recommendations %}
{% set ns = namespace(critical=0) %}
{% for rec in company.recommendations %}{% if 'Brak profili' in rec %}{% set ns.critical = ns.critical + 1 %}{% endif %}{% endfor %}
{% set warnings = company.recommendations|length - ns.critical %}
<span title="{{ company.recommendations|join(', ') }}" style="cursor: help;">
{% if ns.critical %}<span style="color: #991b1b;">&#10060; {{ ns.critical }}</span> {% endif %}
{% if warnings %}<span style="color: #b45309;">&#9888; {{ warnings }}</span>{% endif %}
</span>
{% else %}
<span style="color: #059669;">&#10003;</span>
{% endif %}
</td>
<td class="date-cell hide-mobile">
{% if company.last_verified %}
{% set days_ago = (now - company.last_verified).days %}
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.last_verified.strftime('%Y-%m-%d %H:%M') }}">
{{ company.last_verified.strftime('%d.%m.%Y') }}
</span>
{% else %}
<span class="date-never">-</span>
{% endif %}
</td>
<td>
<div class="action-buttons">
<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"/>
</svg>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<h3>Brak firm do wyświetlenia</h3>
<p>Nie znaleziono firm z danymi social media.</p>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
// Sorting state
let currentSort = { column: 'platforms', direction: 'desc' };
// Sort table
function sortTable(column) {
const tbody = document.getElementById('socialTableBody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const headers = document.querySelectorAll('.social-table th[data-sort]');
// Toggle direction if same column
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'desc';
}
// Update header classes
headers.forEach(h => {
h.classList.remove('sorted', 'sorted-asc', 'sorted-desc');
if (h.dataset.sort === column) {
h.classList.add('sorted', `sorted-${currentSort.direction}`);
}
});
// Sort rows
rows.sort((a, b) => {
let aVal, bVal;
if (column === 'name') {
aVal = a.dataset.name || '';
bVal = b.dataset.name || '';
} else if (column === 'category') {
aVal = a.dataset.category || '';
bVal = b.dataset.category || '';
} else if (column === 'date') {
aVal = new Date(a.dataset.date).getTime();
bVal = new Date(b.dataset.date).getTime();
} else if (column === 'activity') {
// Lower days = more active, sort ascending by default
aVal = parseFloat(a.dataset.activity) || 9999;
bVal = parseFloat(b.dataset.activity) || 9999;
} else {
aVal = parseFloat(a.dataset[column]) || -1;
bVal = parseFloat(b.dataset[column]) || -1;
}
if (aVal < bVal) return currentSort.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return currentSort.direction === 'asc' ? 1 : -1;
return 0;
});
// Re-append rows
rows.forEach(row => tbody.appendChild(row));
}
// Setup sorting click handlers
document.querySelectorAll('.social-table th[data-sort]').forEach(th => {
th.addEventListener('click', () => sortTable(th.dataset.sort));
});
// Filtering
function applyFilters() {
const category = document.getElementById('filterCategory').value;
const platform = document.getElementById('filterPlatform').value;
const activity = document.getElementById('filterActivity').value;
const search = document.getElementById('filterSearch').value.toLowerCase();
const rows = document.querySelectorAll('#socialTableBody tr');
rows.forEach(row => {
let show = true;
// Category filter
if (category && row.dataset.category !== category) {
show = false;
}
// Platform filter
if (platform && show) {
if (platform === 'none') {
if (parseInt(row.dataset.platforms) > 0) show = false;
} else {
const hasPlatform = row.dataset[`has${platform.charAt(0).toUpperCase() + platform.slice(1)}`] === 'true';
if (!hasPlatform) show = false;
}
}
// Activity filter
if (activity && show) {
const days = parseInt(row.dataset.activity) || 9999;
if (activity === 'active' && days > 7) show = false;
else if (activity === 'warning' && (days <= 7 || days > 30)) show = false;
else if (activity === 'inactive' && days <= 30) show = false;
else if (activity === 'unknown' && days !== 9999) show = false;
}
// Search filter
if (search && show) {
if (!row.dataset.name.includes(search)) {
show = false;
}
}
row.style.display = show ? '' : 'none';
});
}
function resetFilters() {
document.getElementById('filterCategory').value = '';
document.getElementById('filterPlatform').value = '';
document.getElementById('filterActivity').value = '';
document.getElementById('filterSearch').value = '';
applyFilters();
}
// Enrichment
var _enrichSince = 0;
var _enrichPendingCount = 0;
function startEnrichment() {
document.getElementById('enrichConfirm').style.display = 'flex';
}
function doStartEnrichment() {
var btn = document.getElementById('enrichBtn');
btn.disabled = true;
btn.textContent = 'Uruchamianie...';
_enrichSince = 0;
_enrichPendingCount = 0;
var platforms = Array.from(document.querySelectorAll('.enrich-platform:checked')).map(function(cb) { return cb.value; });
if (platforms.length === 0) {
btn.disabled = false;
btn.textContent = 'Uruchom audyt';
return;
}
fetch('{{ url_for("admin.admin_social_audit_run_enrichment") }}', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': '{{ csrf_token() }}'},
body: 'platforms=' + platforms.join(','),
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'started') {
// Show live panel
document.getElementById('enrichPanel').style.display = 'block';
document.getElementById('enrichCounter').textContent = '0 / ' + data.total;
document.getElementById('enrichFeed').innerHTML = '';
document.getElementById('enrichMiniStatus').style.display = 'inline';
document.getElementById('enrichMiniStatus').textContent = 'Skanowanie...';
pollEnrichment();
} else {
document.getElementById('enrichPanel').style.display = 'block';
document.getElementById('enrichTitle').textContent = data.error || 'Błąd uruchamiania';
document.getElementById('enrichTitle').style.color = '#dc2626';
document.getElementById('enrichSubtitle').textContent = '';
document.getElementById('enrichSpinner').style.display = 'none';
btn.disabled = false;
btn.textContent = 'Uruchom audyt';
}
})
.catch(function(e) {
document.getElementById('enrichPanel').style.display = 'block';
document.getElementById('enrichTitle').textContent = 'Błąd połączenia: ' + e.message;
document.getElementById('enrichTitle').style.color = '#dc2626';
document.getElementById('enrichSpinner').style.display = 'none';
btn.disabled = false;
btn.textContent = 'Uruchom audyt';
});
}
function _statusIcon(status) {
if (status === 'changes') return '<span style="color:#2563eb;">&#9679;</span>';
if (status === 'skipped') return '<span style="color:#9ca3af;">&#9675;</span>';
if (status === 'error') return '<span style="color:#ef4444;">&#9888;</span>';
if (status === 'no_changes') return '<span style="color:#22c55e;">&#10003;</span>';
return '<span style="color:#9ca3af;">&#8212;</span>';
}
function _platformBadge(p) {
var colors = {
'changes': 'background:#dbeafe;color:#1d4ed8;',
'skipped': 'background:#f3f4f6;color:#6b7280;',
'error': 'background:#fee2e2;color:#991b1b;',
'no_changes': 'background:#f0fdf4;color:#15803d;',
'no_data': 'background:#f3f4f6;color:#9ca3af;'
};
var style = colors[p.status] || 'background:#f3f4f6;color:#6b7280;';
var name = p.platform.charAt(0).toUpperCase() + p.platform.slice(1);
return '<span style="display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;font-size:11px;' + style + '" title="' + p.desc + '">' + name + ': ' + p.desc + '</span>';
}
function pollEnrichment() {
fetch('{{ url_for("admin.admin_social_audit_enrichment_status") }}?since=' + _enrichSince)
.then(function(r) { return r.json(); })
.then(function(data) {
// Update progress
document.getElementById('enrichCounter').textContent = data.completed + ' / ' + data.total;
document.getElementById('enrichBar').style.width = data.progress + '%';
document.getElementById('enrichMiniStatus').textContent = data.completed + '/' + data.total;
// Append new feed entries
var feed = document.getElementById('enrichFeed');
if (data.feed && data.feed.length > 0) {
for (var i = 0; i < data.feed.length; i++) {
var r = data.feed[i];
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;';
var badges = r.profiles.map(_platformBadge).join(' ');
var nameStyle = r.has_changes ? 'font-weight:600;color:#1d4ed8;' : 'color:var(--text-secondary);';
row.innerHTML = '<span style="min-width:24px;text-align:center;">' + (_enrichSince + i + 1) + '.</span>' +
'<span style="min-width:200px;' + nameStyle + '">' + r.company_name + '</span>' +
'<span style="display:flex;gap:4px;flex-wrap:wrap;">' + badges + '</span>';
feed.appendChild(row);
feed.scrollTop = feed.scrollHeight;
}
_enrichSince += data.feed.length;
}
if (data.pending_count > _enrichPendingCount) {
_enrichPendingCount = data.pending_count;
document.getElementById('enrichSubtitle').textContent = _enrichPendingCount + ' profili z nowymi danymi (do zatwierdzenia)';
}
if (data.running) {
setTimeout(pollEnrichment, 2000);
} else {
// Scan complete
document.getElementById('enrichSpinner').style.animation = 'none';
document.getElementById('enrichSpinner').style.borderTopColor = '#22c55e';
document.getElementById('enrichSpinner').style.borderColor = '#22c55e';
var btn = document.getElementById('enrichBtn');
btn.disabled = false;
btn.innerHTML = '<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';
if (data.pending_count > 0) {
document.getElementById('enrichTitle').textContent = 'Skanowanie zakończone — ' + data.pending_count + ' zmian do zatwierdzenia';
document.getElementById('enrichSubtitle').innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;font-weight:600;">Przejdź do raportu &rarr;</a>';
document.getElementById('enrichMiniStatus').innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;">' + data.pending_count + ' zmian &rarr;</a>';
// Add review link row
var linkRow = document.createElement('div');
linkRow.style.cssText = 'padding:12px 0;text-align:center;font-weight:600;';
linkRow.innerHTML = '<a href="{{ url_for("admin.admin_social_audit_enrichment_review") }}" style="color:#2563eb;font-size:14px;">Przejdź do raportu ze zmianami (' + data.pending_count + ' profili) &rarr;</a>';
feed.appendChild(linkRow);
feed.scrollTop = feed.scrollHeight;
} else {
document.getElementById('enrichTitle').textContent = 'Skanowanie zakończone — brak nowych danych';
document.getElementById('enrichSubtitle').textContent = data.errors > 0 ? data.errors + ' błędów' : 'Wszystkie profile aktualne';
document.getElementById('enrichMiniStatus').textContent = 'Zakończono';
}
}
});
}
{% endblock %}