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
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:
parent
94b3e90daf
commit
3a2aa6f33a
@ -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()
|
||||
|
||||
|
||||
@ -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 = '✓ 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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user