feat: show FB API stats on Social Studio + sync button on Integracje
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

Social Publisher: blue gradient panel with followers, engagement,
completeness, contact pills, and refresh button.
Integracje: "Synchronizuj dane" button next to Disconnect for Facebook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 14:21:31 +01:00
parent 94b3e90daf
commit 3a2aa6f33a
3 changed files with 146 additions and 1 deletions

View File

@ -102,6 +102,18 @@ def social_publisher_list():
stats = social_publisher.get_stats()
configured_companies = social_publisher.get_configured_companies(user_company_ids)
# Load Facebook API stats for connected companies
fb_stats = {}
if user_company_ids:
from database import CompanySocialMedia
fb_profiles = db.query(CompanySocialMedia).filter(
CompanySocialMedia.company_id.in_(user_company_ids),
CompanySocialMedia.platform == 'facebook',
CompanySocialMedia.source == 'facebook_api',
).all()
for fp in fb_profiles:
fb_stats[fp.company_id] = fp
return render_template('admin/social_publisher.html',
posts=posts,
stats=stats,
@ -109,7 +121,8 @@ def social_publisher_list():
status_filter=status_filter,
type_filter=type_filter,
company_filter=company_filter,
configured_companies=configured_companies)
configured_companies=configured_companies,
fb_stats=fb_stats)
finally:
db.close()

View File

@ -493,6 +493,10 @@
<p class="oauth-account-name">Strona: {{ fb_conn.account_name }}</p>
{% endif %}
<div class="oauth-card-actions">
<button class="btn-oauth connect" onclick="syncFbData(this)" style="background: #1877f2; color: white; border-color: #1877f2;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path 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>
Synchronizuj dane
</button>
<button class="btn-oauth disconnect" onclick="disconnectOAuth('meta', 'facebook')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg>
Rozłącz
@ -727,4 +731,34 @@
window.history.replaceState({}, document.title, window.location.pathname);
}
})();
function syncFbData(btn) {
var origHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;"><path 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> 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: {{ company.id }}})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
var d = data.data || {};
btn.innerHTML = '&#10003; Zsynchronizowano' + (d.followers_count ? ' (' + d.followers_count + ' obs.)' : '');
btn.style.background = '#10b981';
btn.style.borderColor = '#10b981';
showToast('Dane Facebook zsynchronizowane!', 'success');
} else {
btn.innerHTML = origHTML;
btn.disabled = false;
showToast(data.message || 'Błąd synchronizacji', 'error');
}
})
.catch(function() {
btn.innerHTML = origHTML;
btn.disabled = false;
showToast('Błąd połączenia', 'error');
});
}
{% endblock %}

View File

@ -231,6 +231,77 @@
</div>
</div>
<!-- Facebook Page Stats (from API) -->
{% if fb_stats %}
{% for company_id, fb in fb_stats.items() %}
<div style="background: linear-gradient(135deg, #1877f2 0%, #0d5bbf 100%); border-radius: var(--radius-lg); padding: var(--spacing-lg); margin-bottom: var(--spacing-lg); color: white;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--spacing-md);">
<div style="display: flex; align-items: center; gap: var(--spacing-md);">
<svg width="28" height="28" fill="white" 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 style="font-weight: 700; font-size: var(--font-size-lg);">{{ fb.page_name or 'Strona Facebook' }}</div>
{% if fb.content_types and fb.content_types.get('category') %}
<div style="font-size: var(--font-size-xs); opacity: 0.85;">{{ fb.content_types['category'] }}</div>
{% endif %}
</div>
</div>
<div style="display: flex; gap: var(--spacing-xl); align-items: center; flex-wrap: wrap;">
{% if fb.followers_count %}
<div style="text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ '{:,}'.format(fb.followers_count).replace(',', ' ') }}</div>
<div style="font-size: var(--font-size-xs); opacity: 0.8;">Obserwujących</div>
</div>
{% endif %}
{% if fb.engagement_rate is not none %}
<div style="text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ '%.1f'|format(fb.engagement_rate) }}%</div>
<div style="font-size: var(--font-size-xs); opacity: 0.8;">Engagement</div>
</div>
{% endif %}
{% if fb.profile_completeness_score is not none %}
<div style="text-align: center;">
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ fb.profile_completeness_score }}%</div>
<div style="font-size: var(--font-size-xs); opacity: 0.8;">Profil</div>
</div>
{% endif %}
<button onclick="syncFacebookData({{ company_id }}, this)" style="background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.4); color: white; padding: 6px 14px; border-radius: var(--radius); cursor: pointer; font-size: var(--font-size-xs); transition: background 0.2s;" onmouseover="this.style.background='rgba(255,255,255,0.3)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20" style="vertical-align: -2px; margin-right: 4px;"><path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/></svg>
Odśwież
</button>
</div>
</div>
{% if fb.content_types %}
{% set extras = fb.content_types %}
{% if extras.get('phone') or extras.get('website') or extras.get('address') %}
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid rgba(255,255,255,0.2);">
{% if extras.get('phone') %}
<span style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; background: rgba(255,255,255,0.15); padding: 3px 10px; border-radius: 12px;">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/></svg>
{{ extras['phone'] }}
</span>
{% endif %}
{% if extras.get('website') %}
<a href="{{ extras['website'] }}" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; background: rgba(255,255,255,0.15); padding: 3px 10px; border-radius: 12px; color: white; text-decoration: none;">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/></svg>
{{ extras['website']|replace('https://', '')|replace('http://', '')|truncate(30) }}
</a>
{% endif %}
{% if extras.get('address') %}
<span style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; background: rgba(255,255,255,0.15); padding: 3px 10px; border-radius: 12px;">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17.657 16.657L13.414 20.9a2 2 0 01-2.828 0l-4.243-4.243a8 8 0 1111.314 0z"/><path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
{{ extras['address']|truncate(35) }}
</span>
{% endif %}
</div>
{% endif %}
{% endif %}
{% if fb.last_checked_at %}
<div style="margin-top: var(--spacing-sm); font-size: 11px; opacity: 0.6;">Ostatnia synchronizacja: {{ fb.last_checked_at.strftime('%d.%m.%Y %H:%M') }}</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
<!-- Filtry -->
<div class="filters-row">
<div class="filter-group">
@ -480,4 +551,31 @@
showToast('Błąd połączenia: ' + err.message, 'error');
}
}
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');
});
}
{% endblock %}