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
Display yellow 'Do weryfikacji' badge when check_status is needs_verification, red 'Nieaktywny' when is_valid is false. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
538 lines
18 KiB
HTML
Executable File
538 lines
18 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Social Media Analytics - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.analytics-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.analytics-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card.facebook { border-top: 4px solid #1877f2; }
|
|
.stat-card.linkedin { border-top: 4px solid #0a66c2; }
|
|
.stat-card.instagram { border-top: 4px solid #e4405f; }
|
|
.stat-card.youtube { border-top: 4px solid #ff0000; }
|
|
.stat-card.twitter { border-top: 4px solid #1da1f2; }
|
|
.stat-card.success { border-top: 4px solid var(--success); }
|
|
.stat-card.warning { border-top: 4px solid var(--warning); }
|
|
.stat-card.error { border-top: 4px solid var(--error); }
|
|
|
|
.stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.section {
|
|
background: var(--surface);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.section h2 {
|
|
font-size: var(--font-size-xl);
|
|
margin-bottom: var(--spacing-lg);
|
|
color: var(--text-primary);
|
|
border-bottom: 2px solid var(--border);
|
|
padding-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.combo-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.combo-card {
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.combo-card h3 {
|
|
font-size: var(--font-size-base);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-sm);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.combo-card h3 .count {
|
|
background: var(--primary);
|
|
color: white;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.company-list {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.company-list a {
|
|
display: block;
|
|
padding: var(--spacing-xs) 0;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.company-list a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.company-list a:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.platform-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.platform-badge.facebook { background: #e7f3ff; color: #1877f2; }
|
|
.platform-badge.linkedin { background: #e8f4fc; color: #0a66c2; }
|
|
.platform-badge.instagram { background: #fce7ed; color: #e4405f; }
|
|
.platform-badge.youtube { background: #ffe8e8; color: #ff0000; }
|
|
.platform-badge.twitter { background: #e8f6fc; color: #1da1f2; }
|
|
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.data-table th, .data-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.data-table th {
|
|
background: var(--background);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
|
|
.data-table tr:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.freshness-indicator {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.freshness-indicator.fresh { background: var(--success); }
|
|
.freshness-indicator.stale { background: var(--warning); }
|
|
.freshness-indicator.old { background: var(--error); }
|
|
|
|
.table-container {
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-lg);
|
|
border-bottom: 2px solid var(--border);
|
|
padding-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.tab {
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
cursor: pointer;
|
|
background: var(--background);
|
|
color: var(--text-secondary);
|
|
border: none;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.tab.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.no-sm-list {
|
|
columns: 2;
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.no-sm-list a {
|
|
display: block;
|
|
padding: var(--spacing-xs) 0;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.no-sm-list a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.no-sm-list {
|
|
columns: 1;
|
|
}
|
|
}
|
|
|
|
/* Platform Filter Buttons */
|
|
.platform-filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border: 2px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
transition: all 0.2s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.filter-btn.active {
|
|
background: var(--primary);
|
|
border-color: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.filter-btn.active .platform-badge {
|
|
background: rgba(255,255,255,0.3) !important;
|
|
color: white !important;
|
|
}
|
|
|
|
.filter-btn[data-platform="facebook"].active { background: #1877f2; border-color: #1877f2; }
|
|
.filter-btn[data-platform="instagram"].active { background: #e4405f; border-color: #e4405f; }
|
|
.filter-btn[data-platform="linkedin"].active { background: #0a66c2; border-color: #0a66c2; }
|
|
.filter-btn[data-platform="youtube"].active { background: #ff0000; border-color: #ff0000; }
|
|
.filter-btn[data-platform="twitter"].active { background: #1da1f2; border-color: #1da1f2; }
|
|
.filter-btn[data-platform="tiktok"].active { background: #000000; border-color: #000000; }
|
|
|
|
.data-table tr.hidden-row {
|
|
display: none;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="analytics-header">
|
|
<h1>Social Media Analytics</h1>
|
|
<p class="text-muted">Pelny przeglad kont social media firm czlonkowskich</p>
|
|
</div>
|
|
|
|
<!-- Main Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card success">
|
|
<div class="stat-value">{{ companies_with_sm }}</div>
|
|
<div class="stat-label">Firm z Social Media</div>
|
|
</div>
|
|
<div class="stat-card warning">
|
|
<div class="stat-value">{{ companies_without_sm|length }}</div>
|
|
<div class="stat-label">Firm BEZ Social Media</div>
|
|
</div>
|
|
{% for stat in platform_stats %}
|
|
<div class="stat-card {{ stat.platform }}">
|
|
<div class="stat-value">{{ stat.companies }}</div>
|
|
<div class="stat-label">{{ stat.platform|title }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Freshness Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card success">
|
|
<div class="stat-value">{{ fresh_30d }}</div>
|
|
<div class="stat-label">Zweryfikowane < 30 dni</div>
|
|
</div>
|
|
<div class="stat-card error">
|
|
<div class="stat-value">{{ stale_90d }}</div>
|
|
<div class="stat-label">Nieaktualne > 90 dni</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="section">
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="showTab('combos', this)">Kombinacje platform</button>
|
|
<button class="tab" onclick="showTab('missing', this)">Firmy bez SM ({{ companies_without_sm|length }})</button>
|
|
<button class="tab" onclick="showTab('all', this)">Wszystkie wpisy ({{ all_entries|length }})</button>
|
|
</div>
|
|
|
|
<!-- Tab: Combinations -->
|
|
<div id="tab-combos" class="tab-content active">
|
|
<h2>Kombinacje platform Social Media</h2>
|
|
<div class="combo-grid">
|
|
{% for combo, companies in platform_combos.items() %}
|
|
<div class="combo-card">
|
|
<h3>
|
|
{% for platform in combo.split(', ') %}
|
|
<span class="platform-badge {{ platform }}">{{ platform|upper }}</span>
|
|
{% endfor %}
|
|
<span class="count">{{ companies|length }}</span>
|
|
</h3>
|
|
<div class="company-list">
|
|
{% for company in companies %}
|
|
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Special categories -->
|
|
<h2 style="margin-top: var(--spacing-xl);">Analiza szczegolowa</h2>
|
|
<div class="combo-grid">
|
|
<div class="combo-card">
|
|
<h3>Tylko Facebook <span class="count">{{ only_facebook|length }}</span></h3>
|
|
<div class="company-list">
|
|
{% for c in only_facebook %}
|
|
<a href="{{ url_for('company_detail', company_id=c.id) }}">{{ c.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="combo-card">
|
|
<h3>Tylko LinkedIn <span class="count">{{ only_linkedin|length }}</span></h3>
|
|
<div class="company-list">
|
|
{% for c in only_linkedin %}
|
|
<a href="{{ url_for('company_detail', company_id=c.id) }}">{{ c.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="combo-card">
|
|
<h3>Tylko Instagram <span class="count">{{ only_instagram|length }}</span></h3>
|
|
<div class="company-list">
|
|
{% for c in only_instagram %}
|
|
<a href="{{ url_for('company_detail', company_id=c.id) }}">{{ c.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="combo-card" style="border-left: 4px solid var(--success);">
|
|
<h3>Wszystkie glowne (FB+LI+IG) <span class="count">{{ has_all_major|length }}</span></h3>
|
|
<div class="company-list">
|
|
{% for c in has_all_major %}
|
|
<a href="{{ url_for('company_detail', company_id=c.id) }}">{{ c.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab: Missing -->
|
|
<div id="tab-missing" class="tab-content">
|
|
<h2>Firmy bez danych Social Media ({{ companies_without_sm|length }})</h2>
|
|
<p class="text-muted" style="margin-bottom: var(--spacing-lg);">Te firmy wymagaja uzupelnienia danych o kontach social media.</p>
|
|
<div class="no-sm-list">
|
|
{% for c in companies_without_sm|sort(attribute='name') %}
|
|
<a href="{{ url_for('company_detail', company_id=c.id) }}">{{ c.name }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab: All Entries -->
|
|
<div id="tab-all" class="tab-content">
|
|
<h2>Wszystkie wpisy Social Media</h2>
|
|
|
|
<!-- Platform Filter Buttons -->
|
|
<div class="platform-filters" style="margin-bottom: var(--spacing-lg);">
|
|
<span style="margin-right: var(--spacing-sm); color: var(--text-secondary);">Filtruj platformy:</span>
|
|
<button type="button" class="filter-btn active" data-platform="all" onclick="toggleFilter('all', this)">Wszystkie</button>
|
|
<button type="button" class="filter-btn" data-platform="facebook" onclick="toggleFilter('facebook', this)">
|
|
<span class="platform-badge facebook">FB</span> Facebook
|
|
</button>
|
|
<button type="button" class="filter-btn" data-platform="instagram" onclick="toggleFilter('instagram', this)">
|
|
<span class="platform-badge instagram">IG</span> Instagram
|
|
</button>
|
|
<button type="button" class="filter-btn" data-platform="linkedin" onclick="toggleFilter('linkedin', this)">
|
|
<span class="platform-badge linkedin">LI</span> LinkedIn
|
|
</button>
|
|
<button type="button" class="filter-btn" data-platform="youtube" onclick="toggleFilter('youtube', this)">
|
|
<span class="platform-badge youtube">YT</span> YouTube
|
|
</button>
|
|
<button type="button" class="filter-btn" data-platform="twitter" onclick="toggleFilter('twitter', this)">
|
|
<span class="platform-badge twitter">X</span> Twitter/X
|
|
</button>
|
|
<button type="button" class="filter-btn" data-platform="tiktok" onclick="toggleFilter('tiktok', this)">
|
|
<span style="background:#000;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;margin-right:4px;">TT</span> TikTok
|
|
</button>
|
|
<span id="filter-count" style="margin-left: var(--spacing-md); color: var(--text-secondary);"></span>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="data-table" id="sm-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Firma</th>
|
|
<th>Platforma</th>
|
|
<th>URL</th>
|
|
<th>Zrodlo</th>
|
|
<th>Zweryfikowano</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in all_entries %}
|
|
{% set sm = entry[0] %}
|
|
<tr data-platform="{{ sm.platform }}">
|
|
<td><a href="{{ url_for('company_detail', company_id=sm.company_id) }}">{{ entry.company_name }}</a></td>
|
|
<td><span class="platform-badge {{ sm.platform }}">{{ sm.platform|upper }}</span></td>
|
|
<td><a href="{{ sm.url }}" target="_blank" rel="noopener">{{ sm.url[:50] }}{% if sm.url|length > 50 %}...{% endif %}</a></td>
|
|
<td>{{ sm.source or '-' }}</td>
|
|
<td>
|
|
{% set days_ago = (now - sm.verified_at).days if sm.verified_at else 999 %}
|
|
<span class="freshness-indicator {% if days_ago < 30 %}fresh{% elif days_ago < 90 %}stale{% else %}old{% endif %}"></span>
|
|
{{ sm.verified_at.strftime('%Y-%m-%d') if sm.verified_at else '-' }}
|
|
</td>
|
|
<td>
|
|
{% if sm.check_status == 'needs_verification' %}
|
|
<span style="background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem;">Do weryfikacji</span>
|
|
{% elif not sm.is_valid %}
|
|
<span style="background: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 4px; font-size: 0.8rem;">Nieaktywny</span>
|
|
{% else %}
|
|
OK
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
const now = new Date();
|
|
|
|
// Active platform filters (empty = all)
|
|
let activeFilters = new Set();
|
|
|
|
function showTab(tabName, clickedBtn) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
|
|
|
// Show selected tab
|
|
document.getElementById('tab-' + tabName).classList.add('active');
|
|
if (clickedBtn) clickedBtn.classList.add('active');
|
|
}
|
|
|
|
function toggleFilter(platform, btn) {
|
|
const allBtn = document.querySelector('.filter-btn[data-platform="all"]');
|
|
|
|
if (platform === 'all') {
|
|
// Clear all filters, show everything
|
|
activeFilters.clear();
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
allBtn.classList.add('active');
|
|
} else {
|
|
// Toggle specific platform
|
|
allBtn.classList.remove('active');
|
|
|
|
if (activeFilters.has(platform)) {
|
|
activeFilters.delete(platform);
|
|
btn.classList.remove('active');
|
|
} else {
|
|
activeFilters.add(platform);
|
|
btn.classList.add('active');
|
|
}
|
|
|
|
// If no filters active, activate "all"
|
|
if (activeFilters.size === 0) {
|
|
allBtn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
applyFilters();
|
|
}
|
|
|
|
function applyFilters() {
|
|
const table = document.getElementById('sm-table');
|
|
if (!table) return;
|
|
|
|
const rows = table.querySelectorAll('tbody tr');
|
|
let visibleCount = 0;
|
|
|
|
rows.forEach(row => {
|
|
const platform = row.dataset.platform;
|
|
|
|
if (activeFilters.size === 0 || activeFilters.has(platform)) {
|
|
row.classList.remove('hidden-row');
|
|
visibleCount++;
|
|
} else {
|
|
row.classList.add('hidden-row');
|
|
}
|
|
});
|
|
|
|
// Update count display
|
|
const countEl = document.getElementById('filter-count');
|
|
if (countEl) {
|
|
if (activeFilters.size === 0) {
|
|
countEl.textContent = '';
|
|
} else {
|
|
countEl.textContent = `Pokazano: ${visibleCount} z ${rows.length}`;
|
|
}
|
|
}
|
|
}
|
|
{% endblock %}
|