{% extends "base.html" %} {% block title %}Social Media Dashboard - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Social Media Dashboard

{{ stats.total or 0 }}
Wszystkie
{{ stats.draft or 0 }}
Szkice
{{ stats.approved or 0 }}
Zatwierdzone
{{ stats.scheduled or 0 }}
Zaplanowane
{{ stats.published or 0 }}
Opublikowane
{{ stats.failed or 0 }}
Bledy
{% if fb_stats %} {% for company_id, fb in fb_stats.items() %}
{{ fb.page_name or 'Strona Facebook' }}
{% if fb.content_types and fb.content_types.get('category') %}
{{ fb.content_types['category'] }}
{% endif %}
{% if fb.followers_count %}
{{ '{:,}'.format(fb.followers_count).replace(',', ' ') }}
Obserwujących
{% endif %} {% if fb.engagement_rate is not none %}
{{ '%.1f'|format(fb.engagement_rate) }}%
Engagement
{% endif %} {% if fb.profile_completeness_score is not none %}
{{ fb.profile_completeness_score }}%
Profil
{% endif %}
{% if fb.content_types %} {% set extras = fb.content_types %} {% if extras.get('phone') or extras.get('website') or extras.get('address') %}
{% if extras.get('phone') %} {{ extras['phone'] }} {% endif %} {% if extras.get('website') %} {{ extras['website']|replace('https://', '')|replace('http://', '')|truncate(30) }} {% endif %} {% if extras.get('address') %} {{ extras['address']|truncate(35) }} {% endif %}
{% endif %} {% endif %} {% if fb.last_checked_at %}
Ostatnia synchronizacja: {{ fb.last_checked_at|local_time('%d.%m.%Y %H:%M') }}
{% endif %}
Publikowanie nie działa? Zmiana hasła FB lub usunięcie aplikacji wymaga ponownego połączenia w Integracje.
{% endfor %} {% for company_id_key, fb in fb_stats.items() %}

Analityka Facebook {% if cached_fb_posts.get(company_id_key) %} ({{ cached_fb_posts[company_id_key].total_count }} postów) {% endif %}

Top 5 postów

{% endfor %} {% endif %}

Posty tworzone w aplikacji

{% if configured_companies %}
{% endif %}
{% if posts %} {% for post in posts %} {% endfor %}
Typ Tresc Firma Publikuje jako Status Data Engagement Akcje
{{ post_types.get(post.post_type, post.post_type) }} {{ post.content[:80] }}{% if post.content|length > 80 %}...{% endif %} {{ post.company.name if post.company else '-' }} {{ post.publishing_company.name if post.publishing_company else '-' }} {% if post.status == 'draft' %}Szkic {% elif post.status == 'approved' %}Zatwierdzony {% elif post.status == 'scheduled' %}Zaplanowany {% elif post.status == 'published' %}Opublikowany {% elif post.status == 'failed' %}Błąd {% else %}{{ post.status }}{% endif %} {% if post.published_at %} {{ post.published_at|local_time('%Y-%m-%d %H:%M') }} {% elif post.scheduled_at %} {{ post.scheduled_at|local_time('%Y-%m-%d %H:%M') }} {% else %} {{ post.created_at|local_time('%Y-%m-%d %H:%M') if post.created_at else '-' }} {% endif %} Edytuj {% if post.status == 'draft' %}
{% endif %} {% if post.status in ['draft', 'approved'] %} {% endif %} {% if post.status != 'published' %} {% endif %}
{% else %}

Brak postów{% if status_filter != 'all' or type_filter != 'all' %} pasujących do filtrów{% endif %}.

Utwórz pierwszy post

{% endif %}
{% if fb_stats %} {% for company_id_key, fb in fb_stats.items() %}

Posty z Facebooka {% if cached_fb_posts.get(company_id_key) %} ({{ cached_fb_posts[company_id_key].total_count }} postów, cache z {{ cached_fb_posts[company_id_key].cached_at|local_time('%d.%m %H:%M') }}) {% endif %}

{% endfor %} {% endif %}
{% endblock %} {% block extra_js %} function applyFilters() { const status = document.getElementById('status-filter').value; const type = document.getElementById('type-filter').value; const companyEl = document.getElementById('company-filter'); const company = companyEl ? companyEl.value : 'all'; let url = '{{ url_for("admin.social_publisher_list") }}?'; if (status !== 'all') url += 'status=' + status + '&'; if (type !== 'all') url += 'type=' + type + '&'; if (company !== 'all') url += 'company=' + company + '&'; window.location.href = url; } let confirmResolve = null; function showConfirm(message, options = {}) { return new Promise(resolve => { confirmResolve = resolve; document.getElementById('confirmModalIcon').innerHTML = options.icon || '❓'; document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; document.getElementById('confirmModalMessage').innerHTML = message; document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); document.getElementById('confirmModal').classList.add('active'); }); } function closeConfirm(result) { document.getElementById('confirmModal').classList.remove('active'); if (confirmResolve) { confirmResolve(result); confirmResolve = null; } } document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); function showToast(message, type = 'info', duration = 4000) { const container = document.getElementById('toastContainer'); const icons = { success: '✓', error: '✗', warning: '⚠', info: 'ℹ' }; const toast = document.createElement('div'); toast.className = 'toast ' + type; toast.innerHTML = '' + (icons[type]||'ℹ') + '' + message + ''; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); } async function publishPost(id) { const confirmed = await showConfirm('Czy na pewno chcesz opublikować ten post na Facebook?', { icon: '📣', title: 'Publikacja posta', okText: 'Publikuj', okClass: 'btn-success' }); if (!confirmed) return; try { const response = await fetch('{{ url_for("admin.social_publisher_publish", post_id=0) }}'.replace('/0/', '/' + id + '/'), { method: 'POST', headers: { 'X-CSRFToken': '{{ csrf_token() }}' } }); const data = await response.json(); if (data.success) { showToast('Post został opublikowany na Facebook', 'success'); setTimeout(() => location.reload(), 1500); } else { showToast('Błąd: ' + (data.error || 'Nieznany błąd'), 'error'); } } catch (err) { showToast('Błąd połączenia: ' + err.message, 'error'); } } async function deletePost(id) { const confirmed = await showConfirm('Czy na pewno chcesz usunąć ten post? Ta operacja jest nieodwracalna.', { icon: '🗑', title: 'Usuwanie posta', okText: 'Usuń', okClass: 'btn-error' }); if (!confirmed) return; try { const response = await fetch('{{ url_for("admin.social_publisher_delete", post_id=0) }}'.replace('/0/', '/' + id + '/'), { method: 'POST', headers: { 'X-CSRFToken': '{{ csrf_token() }}' } }); const data = await response.json(); if (data.success) { showToast('Post został usunięty', 'success'); setTimeout(() => location.reload(), 1500); } else { showToast('Błąd: ' + (data.error || 'Nieznany błąd'), 'error'); } } catch (err) { showToast('Błąd połączenia: ' + err.message, 'error'); } } // Collapsible sections with localStorage persistence function toggleSection(sectionId) { var body = document.getElementById(sectionId + '-body'); var header = body ? body.previousElementSibling : null; if (!body) return; var collapsed = !body.classList.contains('collapsed'); body.classList.toggle('collapsed'); if (header && header.classList.contains('collapsible-header')) { header.classList.toggle('collapsed', collapsed); } try { localStorage.setItem('sp_' + sectionId, collapsed ? '0' : '1'); } catch(e) {} } // Restore collapsed state from localStorage document.querySelectorAll('.collapsible-header').forEach(function(h) { var body = h.nextElementSibling; if (!body || !body.id) return; var key = 'sp_' + body.id.replace('-body', ''); try { var saved = localStorage.getItem(key); if (saved === '0') { body.classList.add('collapsed'); h.classList.add('collapsed'); } } catch(e) {} }); function formatFbDate(isoStr) { if (!isoStr) return ''; var d = new Date(isoStr); return d.toLocaleDateString('pl-PL', {day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'}); } // Store all posts per company for client-side pagination window._fbAllPosts = window._fbAllPosts || {}; window._fbPostsPage = window._fbPostsPage || {}; window._fbPostsPerPage = window._fbPostsPerPage || {}; function renderFbPosts(companyId, posts, nextCursor, append) { var container = document.getElementById('fbPostsContainer-' + companyId); // Store posts; only reset page if new dataset is different size var prevLen = (window._fbAllPosts[companyId] || []).length; window._fbAllPosts[companyId] = posts; if (posts.length !== prevLen) window._fbPostsPage[companyId] = 1; if (!window._fbPostsPerPage[companyId]) window._fbPostsPerPage[companyId] = 10; renderFbPostsPage(companyId); } function renderFbPostsPage(companyId) { var container = document.getElementById('fbPostsContainer-' + companyId); var allPosts = window._fbAllPosts[companyId] || []; var perPage = (companyId in window._fbPostsPerPage) ? window._fbPostsPerPage[companyId] : 10; var page = window._fbPostsPage[companyId] || 1; // Calculate slice var showAll = (perPage === 0); var visible = showAll ? allPosts : allPosts.slice(0, page * perPage); var hasMore = !showAll && (page * perPage < allPosts.length); var html = ''; visible.forEach(function(post) { html += '
'; if (post.full_picture) { html += ''; } html += '
'; html += ''; if (post.message) { html += '
' + post.message.replace(//g, '>') + '
'; } else { html += '
(post bez tekstu)
'; } var engScore = (post.reactions_total || 0) + (post.comments || 0) * 2 + (post.shares || 0) * 3; html += '
'; html += '❤️ ' + (post.reactions_total || 0) + ''; html += '💬 ' + (post.comments || 0) + ''; html += '🔁 ' + (post.shares || 0) + ''; html += '⚡ ' + engScore + ''; html += '
'; html += '
'; if (post.permalink_url) { html += 'Zobacz na FB'; } html += ''; html += '
'; html += ''; html += '
'; }); // Pagination bar helper function paginationBar(margin) { var bar = '
'; bar += 'Wyświetlono ' + visible.length + ' z ' + allPosts.length + ' postów'; bar += '
'; bar += ''; bar += ''; if (hasMore) { bar += ''; } bar += '
'; return bar; } container.innerHTML = paginationBar('margin-bottom:var(--spacing-md)') + html + paginationBar('margin-top:var(--spacing-md)'); } function changeFbPerPage(companyId, value) { window._fbPostsPerPage[companyId] = parseInt(value); window._fbPostsPage[companyId] = 1; renderFbPostsPage(companyId); } function loadMoreFbPosts(companyId) { window._fbPostsPage[companyId] = (window._fbPostsPage[companyId] || 1) + 1; renderFbPostsPage(companyId); } function loadFbPosts(companyId, btn, afterCursor) { var origText = btn.textContent; btn.disabled = true; btn.textContent = 'Ładowanie...'; var container = document.getElementById('fbPostsContainer-' + companyId); var isAppend = !!afterCursor; if (!isAppend) { container.innerHTML = '
Pobieranie postów z Facebook API...
'; } var url = '/admin/social-publisher/fb-posts/' + companyId; if (afterCursor) url += '?after=' + encodeURIComponent(afterCursor); fetch(url) .then(function(r) { return r.json(); }) .then(function(data) { btn.textContent = origText; btn.disabled = false; if (!data.success) { if (!isAppend) { container.innerHTML = '
' + (data.error || 'Błąd') + '
'; } else { showToast(data.error || 'Błąd pobierania', 'error'); } return; } if (!data.posts || data.posts.length === 0) { if (!isAppend) { container.innerHTML = '
Brak postów na stronie.
'; } else { var oldMore = document.getElementById('fbLoadMore-' + companyId); if (oldMore) oldMore.innerHTML = 'To już wszystkie posty.'; } return; } renderFbPosts(companyId, data.posts, data.next_cursor, isAppend); }) .catch(function(err) { btn.textContent = origText; btn.disabled = false; if (!isAppend) { container.innerHTML = '
Błąd połączenia: ' + err.message + '
'; } else { showToast('Błąd połączenia: ' + err.message, 'error'); } }); } function loadPostInsights(companyId, postId, btn) { var safeId = postId.replace(/\./g, '-'); var container = document.getElementById('insights-' + safeId); if (!container) return; if (container.style.display !== 'none') { container.style.display = 'none'; btn.textContent = 'Insights'; return; } btn.disabled = true; btn.textContent = 'Ładowanie...'; container.innerHTML = '
Pobieranie statystyk...
'; container.style.display = 'block'; fetch('/admin/social-publisher/fb-post-insights/' + companyId + '/' + postId) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.textContent = 'Insights'; if (!data.success) { container.innerHTML = '
' + (data.error || 'Brak danych') + '
'; return; } var ins = data.insights; var html = '
'; html += '👁 Wyswietlenia: ' + (ins.impressions != null ? ins.impressions : '-') + ''; html += '📊 Zasieg: ' + (ins.reach != null ? ins.reach : '-') + ''; html += '👥 Zaangazowani: ' + (ins.engaged_users != null ? ins.engaged_users : '-') + ''; html += '🖱️ Klikniecia: ' + (ins.clicks != null ? ins.clicks : '-') + ''; html += '
'; container.innerHTML = html; }) .catch(function(err) { btn.disabled = false; btn.textContent = 'Insights'; container.innerHTML = '
Błąd: ' + err.message + '
'; }); } // Store chart instances for cleanup window._fbCharts = window._fbCharts || {}; async function loadAllFbPosts(companyId, btn) { var origText = btn.textContent; btn.disabled = true; var container = document.getElementById('fbPostsContainer-' + companyId); // Try DB cache first (instant) btn.textContent = 'Ładowanie...'; try { var cacheR = await fetch('/admin/social-publisher/fb-posts-cache/' + companyId); var cacheData = await cacheR.json(); if (cacheData.success && cacheData.posts && cacheData.posts.length > 0) { btn.textContent = origText; btn.disabled = false; renderFbPosts(companyId, cacheData.posts, null, false); container.insertAdjacentHTML('beforeend', '
Wyświetlono ' + cacheData.posts.length + ' postów z cache
'); renderFbCharts(companyId, cacheData.posts, cacheData.cached_at); return; } } catch(e) {} // No cache — fetch all pages from FB API container.innerHTML = '
Pierwsze ładowanie — pobieranie postów z Facebook API...
'; var allPosts = []; var seenIds = {}; var seenCursors = {}; var cursor = null; var page = 0; var MAX_PAGES = 100; try { while (page < MAX_PAGES) { page++; btn.textContent = 'Ładowanie... (' + allPosts.length + ' postów, strona ' + page + ')'; var url = '/admin/social-publisher/fb-posts/' + companyId; if (cursor) url += '?after=' + encodeURIComponent(cursor); var r = await fetch(url); var data = await r.json(); if (!data.success) { showToast(data.error || 'Błąd pobierania', 'error'); break; } if (!data.posts || data.posts.length === 0) break; // Deduplicate by post ID var newPosts = data.posts.filter(function(p) { if (seenIds[p.id]) return false; seenIds[p.id] = true; return true; }); if (newPosts.length === 0) break; // all duplicates = cycle allPosts = allPosts.concat(newPosts); if (!data.next_cursor) break; // Detect cursor cycle if (seenCursors[data.next_cursor]) break; seenCursors[data.next_cursor] = true; cursor = data.next_cursor; } btn.textContent = origText; btn.disabled = false; if (allPosts.length === 0) { container.innerHTML = '
Brak postów na stronie.
'; return; } renderFbPosts(companyId, allPosts, null, false); container.insertAdjacentHTML('beforeend', '
Załadowano ' + allPosts.length + ' postów z Facebook API
'); renderFbCharts(companyId, allPosts, 'teraz (świeżo pobrane)'); saveFbPostsToCache(companyId, allPosts); } catch (err) { btn.textContent = origText; btn.disabled = false; showToast('Błąd połączenia: ' + err.message, 'error'); } } async function refreshAllFbPosts(companyId, btn) { var origText = btn.textContent; btn.disabled = true; var container = document.getElementById('fbPostsContainer-' + companyId); container.innerHTML = '
Pobieranie WSZYSTKICH postów z Facebook API...
'; var allPosts = []; var seenIds = {}; var seenCursors = {}; var cursor = null; var page = 0; var MAX_PAGES = 100; try { while (page < MAX_PAGES) { page++; btn.textContent = 'Pobieranie... (' + allPosts.length + ' postów, strona ' + page + ')'; var url = '/admin/social-publisher/fb-posts/' + companyId; if (cursor) url += '?after=' + encodeURIComponent(cursor); var response = await fetch(url); var data = await response.json(); if (!data.success || !data.posts || data.posts.length === 0) break; var newPosts = data.posts.filter(function(p) { if (seenIds[p.id]) return false; seenIds[p.id] = true; return true; }); if (newPosts.length === 0) break; allPosts = allPosts.concat(newPosts); if (!data.next_cursor) break; if (seenCursors[data.next_cursor]) break; seenCursors[data.next_cursor] = true; cursor = data.next_cursor; } btn.textContent = origText; btn.disabled = false; if (allPosts.length === 0) { container.innerHTML = '
Brak postów.
'; return; } renderFbPosts(companyId, allPosts, null, false); container.insertAdjacentHTML('beforeend', '
Pobrano ' + allPosts.length + ' postów z Facebook API i zapisano do cache
'); renderFbCharts(companyId, allPosts, 'teraz (świeżo pobrane)'); saveFbPostsToCache(companyId, allPosts); } catch (err) { btn.textContent = origText; btn.disabled = false; showToast('Błąd połączenia: ' + err.message, 'error'); } } function renderFbCharts(companyId, posts, cachedAt) { // Show cache date info var infoEl = document.getElementById('fbChartsInfo-' + companyId); if (infoEl) { var dateStr = cachedAt || 'teraz'; infoEl.innerHTML = 'Wykresy na podstawie ' + posts.length + ' postów | Dane z: ' + dateStr + ' | Kliknij Odśwież wszystkie aby zaktualizować'; } // Sort chronologically (oldest first) var sorted = posts.slice().sort(function(a, b) { return new Date(a.created_time) - new Date(b.created_time); }); // Aggregate per month var months = {}; sorted.forEach(function(p) { var d = new Date(p.created_time); var key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); if (!months[key]) months[key] = { count: 0, likes: 0, comments: 0, shares: 0, reactions: 0 }; months[key].count++; months[key].likes += (p.likes || 0); months[key].comments += (p.comments || 0); months[key].shares += (p.shares || 0); months[key].reactions += (p.reactions_total || 0); }); var monthKeys = Object.keys(months).sort(); // Destroy old charts if (window._fbCharts[companyId]) { window._fbCharts[companyId].forEach(function(c) { c.destroy(); }); } window._fbCharts[companyId] = []; var baseOpts = { responsive: true, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 10, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 10 }, maxRotation: 45 } }, y: { beginAtZero: true, ticks: { font: { size: 10 } } } } }; // Chart 1: Engagement per post (line) var ctx1 = document.getElementById('engagementChart-' + companyId).getContext('2d'); var postLabels = sorted.map(function(p) { var d = new Date(p.created_time); return d.toLocaleDateString('pl-PL', {day: '2-digit', month: '2-digit'}); }); var chart1 = new Chart(ctx1, { type: 'line', data: { labels: postLabels, datasets: [ { label: 'Reakcje', data: sorted.map(function(p) { return p.reactions_total || 0; }), borderColor: '#e8a819', backgroundColor: 'rgba(232,168,25,0.1)', tension: 0.3, pointRadius: 2 }, { label: 'Komentarze', data: sorted.map(function(p) { return p.comments || 0; }), borderColor: '#42b72a', backgroundColor: 'rgba(66,183,42,0.1)', tension: 0.3, pointRadius: 2 }, { label: 'Udostepnienia', data: sorted.map(function(p) { return p.shares || 0; }), borderColor: '#f5533d', backgroundColor: 'rgba(245,83,61,0.1)', tension: 0.3, pointRadius: 2 } ] }, options: Object.assign({}, baseOpts, { plugins: Object.assign({}, baseOpts.plugins, { title: { display: false } }) }) }); window._fbCharts[companyId].push(chart1); // Chart 2: Posts per month (bar) var ctx2 = document.getElementById('activityChart-' + companyId).getContext('2d'); var chart2 = new Chart(ctx2, { type: 'bar', data: { labels: monthKeys, datasets: [{ label: 'Liczba postów', data: monthKeys.map(function(k) { return months[k].count; }), backgroundColor: 'rgba(24,119,242,0.7)', borderColor: '#1877f2', borderWidth: 1, borderRadius: 4 }] }, options: baseOpts }); window._fbCharts[companyId].push(chart2); // Chart 3: Avg total engagement per month (line) var ctx3 = document.getElementById('avgEngagementChart-' + companyId).getContext('2d'); var chart3 = new Chart(ctx3, { type: 'line', data: { labels: monthKeys, datasets: [{ label: 'Średni engagement / post', data: monthKeys.map(function(k) { var m = months[k]; var total = m.reactions + m.comments + m.shares; return m.count > 0 ? Math.round(total / m.count * 10) / 10 : 0; }), borderColor: '#e8a819', backgroundColor: 'rgba(232,168,25,0.15)', tension: 0.3, fill: true, pointRadius: 3 }] }, options: baseOpts }); window._fbCharts[companyId].push(chart3); // Chart 4: Post types (doughnut) var typeCounts = {}; sorted.forEach(function(p) { var t = p.status_type || 'other'; typeCounts[t] = (typeCounts[t] || 0) + 1; }); var typeLabels = Object.keys(typeCounts); var typeColors = ['#1877f2', '#42b72a', '#f5533d', '#e8a819', '#8b5cf6', '#ec4899', '#06b6d4']; var ctx4 = document.getElementById('postTypesChart-' + companyId).getContext('2d'); var chart4 = new Chart(ctx4, { type: 'doughnut', data: { labels: typeLabels, datasets: [{ data: typeLabels.map(function(t) { return typeCounts[t]; }), backgroundColor: typeColors.slice(0, typeLabels.length), borderWidth: 2, borderColor: 'var(--surface)' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 8, font: { size: 11 } } } } } }); window._fbCharts[companyId].push(chart4); // Chart 5: Best day of week (bar — avg engagement per day) var dayNames = ['Niedziela', 'Poniedzialek', 'Wtorek', 'Sroda', 'Czwartek', 'Piatek', 'Sobota']; var dayData = Array.from({length: 7}, function() { return { eng: 0, count: 0 }; }); sorted.forEach(function(p) { var dow = new Date(p.created_time).getDay(); dayData[dow].count++; dayData[dow].eng += (p.reactions_total || 0) + (p.comments || 0) * 2 + (p.shares || 0) * 3; }); // Reorder: Mon-Sun var dayOrder = [1, 2, 3, 4, 5, 6, 0]; var ctx5 = document.getElementById('bestDayChart-' + companyId).getContext('2d'); var chart5 = new Chart(ctx5, { type: 'bar', data: { labels: dayOrder.map(function(i) { return dayNames[i]; }), datasets: [ { label: 'Średni engagement', data: dayOrder.map(function(i) { return dayData[i].count > 0 ? Math.round(dayData[i].eng / dayData[i].count * 10) / 10 : 0; }), backgroundColor: 'rgba(24,119,242,0.7)', borderColor: '#1877f2', borderWidth: 1, borderRadius: 4, yAxisID: 'y' }, { label: 'Liczba postów', data: dayOrder.map(function(i) { return dayData[i].count; }), backgroundColor: 'rgba(232,168,25,0.5)', borderColor: '#e8a819', borderWidth: 1, borderRadius: 4, yAxisID: 'y1' } ] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 8, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 10 } } }, y: { beginAtZero: true, position: 'left', title: { display: true, text: 'Avg engagement', font: { size: 10 } }, ticks: { font: { size: 10 } } }, y1: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: 'Posty', font: { size: 10 } }, ticks: { font: { size: 10 } } } } } }); window._fbCharts[companyId].push(chart5); // Chart 6: Best hour (bar — avg engagement per hour) var hourData = Array.from({length: 24}, function() { return { eng: 0, count: 0 }; }); sorted.forEach(function(p) { var h = new Date(p.created_time).getHours(); hourData[h].count++; hourData[h].eng += (p.reactions_total || 0) + (p.comments || 0) * 2 + (p.shares || 0) * 3; }); var hourLabels = []; var hourAvg = []; var hourCounts = []; for (var h = 0; h < 24; h++) { if (hourData[h].count > 0) { hourLabels.push(String(h).padStart(2, '0') + ':00'); hourAvg.push(Math.round(hourData[h].eng / hourData[h].count * 10) / 10); hourCounts.push(hourData[h].count); } } var ctx6 = document.getElementById('bestHourChart-' + companyId).getContext('2d'); var chart6 = new Chart(ctx6, { type: 'bar', data: { labels: hourLabels, datasets: [ { label: 'Średni engagement', data: hourAvg, backgroundColor: 'rgba(66,183,42,0.7)', borderColor: '#42b72a', borderWidth: 1, borderRadius: 4, yAxisID: 'y' }, { label: 'Liczba postów', data: hourCounts, backgroundColor: 'rgba(232,168,25,0.5)', borderColor: '#e8a819', borderWidth: 1, borderRadius: 4, yAxisID: 'y1' } ] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 8, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 10 } } }, y: { beginAtZero: true, position: 'left', title: { display: true, text: 'Avg engagement', font: { size: 10 } }, ticks: { font: { size: 10 } } }, y1: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: 'Posty', font: { size: 10 } }, ticks: { font: { size: 10 } } } } } }); window._fbCharts[companyId].push(chart6); // Chart 7: Top 5 posts (horizontal bar) var scored = sorted.map(function(p) { return { label: (p.message || '(bez tekstu)').substring(0, 60) + (p.message && p.message.length > 60 ? '...' : ''), score: (p.reactions_total || 0) + (p.comments || 0) * 2 + (p.shares || 0) * 3, reactions: p.reactions_total || 0, comments: p.comments || 0, shares: p.shares || 0, url: p.permalink_url || null }; }).sort(function(a, b) { return b.score - a.score; }).slice(0, 5); // Split labels into 2-line arrays for compact display var topLabels = scored.map(function(p) { var t = p.label; if (t.length > 30) return [t.substring(0, 30), t.substring(30)]; return [t]; }); var ctx7 = document.getElementById('topPostsChart-' + companyId).getContext('2d'); var chart7 = new Chart(ctx7, { type: 'bar', data: { labels: topLabels, datasets: [ { label: 'Reakcje', data: scored.map(function(p) { return p.reactions; }), backgroundColor: '#e8a819', borderRadius: 3, barThickness: 14 }, { label: 'Komentarze', data: scored.map(function(p) { return p.comments; }), backgroundColor: '#42b72a', borderRadius: 3, barThickness: 14 }, { label: 'Udostepnienia', data: scored.map(function(p) { return p.shares; }), backgroundColor: '#f5533d', borderRadius: 3, barThickness: 14 } ] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 6, font: { size: 10 } } } }, scales: { x: { stacked: true, beginAtZero: true, ticks: { font: { size: 10 } } }, y: { stacked: true, ticks: { font: { size: 9 }, autoSkip: false } } }, onClick: function(evt, elements) { if (elements.length > 0) { var idx = elements[0].index; var url = scored[idx].url; if (url) window.open(url, '_blank'); } }, onHover: function(evt, elements) { evt.native.target.style.cursor = elements.length > 0 ? 'pointer' : 'default'; } } }); window._fbCharts[companyId].push(chart7); // Show charts and top5 sections document.getElementById('fbChartsSection-' + companyId).style.display = 'block'; var top5El = document.getElementById('fbTop5Section-' + companyId); if (top5El) top5El.style.display = 'block'; } function syncFacebookData(companyId, btn) { var origText = btn.innerHTML; btn.disabled = true; btn.textContent = 'Synchronizuję...'; fetch('/api/oauth/meta/sync-facebook', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''}, body: JSON.stringify({company_id: companyId}) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { btn.textContent = 'Zaktualizowano!'; setTimeout(function() { location.reload(); }, 800); } else { btn.innerHTML = origText; btn.disabled = false; showToast(data.message || 'Błąd synchronizacji', 'error'); } }) .catch(function() { btn.innerHTML = origText; btn.disabled = false; showToast('Błąd połączenia', 'error'); }); } function saveFbPostsToCache(companyId, posts) { fetch('/admin/social-publisher/fb-posts-cache/' + companyId, { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''}, body: JSON.stringify({posts: posts}) }).catch(function() {}); } // Render cached posts preview + auto-load charts from DB cache document.addEventListener('DOMContentLoaded', function() { {% if cached_fb_posts %} {% for cid, cache_data in cached_fb_posts.items() %} (function() { var companyId = {{ cid }}; var cachedPosts = {{ cache_data.posts | tojson }}; var total = {{ cache_data.total_count }}; if (cachedPosts && cachedPosts.length > 0) { renderFbPosts(companyId, cachedPosts, null, false); } // Auto-load full cache via AJAX for charts (fast, from DB) if (total > 0) { fetch('/admin/social-publisher/fb-posts-cache/' + companyId) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success && data.posts && data.posts.length > 0) { renderFbPosts(companyId, data.posts, null, false); renderFbCharts(companyId, data.posts, data.cached_at); } }).catch(function() {}); } })(); {% endfor %} {% endif %} }); {% endblock %}