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
- Expandable help section on Integracje page explaining when connection may stop working (password change, app removal, role loss) - Troubleshooting link on Social Publisher stats panel - Actionable error message on failed posts pointing to Integracje Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
781 lines
34 KiB
HTML
781 lines
34 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Ustawienia firmy - {{ company.name }} - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
.settings-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.settings-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.settings-header .breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
margin-bottom: var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.settings-header .breadcrumb a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.settings-header .breadcrumb a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.settings-header .breadcrumb svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.settings-subtitle {
|
|
margin: var(--spacing-xs) 0 0 0;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* OAuth Cards Grid */
|
|
.oauth-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.oauth-card {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
padding: var(--spacing-lg);
|
|
border: 1px solid var(--border);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.oauth-card:hover {
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.oauth-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.oauth-card-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.oauth-card-icon.google {
|
|
background: #f0f7ff;
|
|
color: #4285F4;
|
|
}
|
|
|
|
.oauth-card-icon.meta {
|
|
background: #f0f0ff;
|
|
color: #1877F2;
|
|
}
|
|
|
|
.oauth-card-icon svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.oauth-card-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.oauth-card-desc {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin: 0 0 var(--spacing-md) 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Status badges */
|
|
.oauth-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.oauth-status.connected {
|
|
background: var(--success-light, #dcfce7);
|
|
color: var(--success, #16a34a);
|
|
}
|
|
|
|
.oauth-status.unavailable {
|
|
background: var(--surface);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.oauth-status.expired {
|
|
background: var(--warning-light, #fef3c7);
|
|
color: var(--warning, #d97706);
|
|
}
|
|
|
|
.oauth-status svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.oauth-account-name {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.oauth-card-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-oauth {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
border: none;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-oauth.connect {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-oauth.connect:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.btn-oauth.disconnect {
|
|
background: transparent;
|
|
color: var(--error, #dc2626);
|
|
border: 1px solid var(--error, #dc2626);
|
|
}
|
|
|
|
.btn-oauth.disconnect:hover {
|
|
background: var(--error, #dc2626);
|
|
color: white;
|
|
}
|
|
|
|
.btn-oauth.discover {
|
|
background: transparent;
|
|
color: var(--primary);
|
|
border: 1px solid var(--primary);
|
|
}
|
|
|
|
.btn-oauth.discover:hover {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-oauth:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-oauth svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
/* Toast notification */
|
|
.toast {
|
|
position: fixed;
|
|
top: var(--spacing-lg);
|
|
right: var(--spacing-lg);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
z-index: 9999;
|
|
animation: toastIn 0.3s ease-out;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.toast.success {
|
|
background: var(--success, #16a34a);
|
|
color: white;
|
|
}
|
|
|
|
.toast.error {
|
|
background: var(--error, #dc2626);
|
|
color: white;
|
|
}
|
|
|
|
@keyframes toastIn {
|
|
from { opacity: 0; transform: translateY(-20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Section title */
|
|
.section-title {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin: 0 0 var(--spacing-md) 0;
|
|
}
|
|
|
|
.section-desc {
|
|
color: var(--text-secondary);
|
|
margin: 0 0 var(--spacing-lg) 0;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* Back link */
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.back-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.back-link svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
/* Disconnect confirm modal */
|
|
.disconnect-modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.disconnect-modal.active { display: flex; }
|
|
.disconnect-modal-content {
|
|
background: var(--surface);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
max-width: 420px;
|
|
width: 90%;
|
|
box-shadow: var(--shadow-lg);
|
|
text-align: center;
|
|
}
|
|
.disconnect-modal-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto var(--spacing-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
background: #FEF3C7;
|
|
color: #F59E0B;
|
|
}
|
|
.disconnect-modal-icon svg { width: 24px; height: 24px; }
|
|
.disconnect-modal-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
.disconnect-modal-desc {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
line-height: 1.5;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
.disconnect-modal-footer {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<!-- Breadcrumb -->
|
|
<div class="settings-header">
|
|
<div class="breadcrumb">
|
|
{% if is_user_view %}
|
|
<a href="{{ url_for('auth.konto_dane') }}">Moje konto</a>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
<span>Integracje</span>
|
|
{% else %}
|
|
<a href="{{ url_for('admin.admin_companies') }}">Firmy</a>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
<a href="{{ url_for('admin.admin_company_get', company_id=company.id) }}">{{ company.name }}</a>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
<span>Ustawienia</span>
|
|
{% endif %}
|
|
</div>
|
|
<h1>{% if is_user_view %}Integracje{% else %}Ustawienia firmy{% endif %}</h1>
|
|
<p class="settings-subtitle">{{ company.name }} — integracje z zewnętrznymi serwisami</p>
|
|
</div>
|
|
|
|
<!-- OAuth Integrations Section -->
|
|
<h2 class="section-title">Połączenia OAuth</h2>
|
|
<p class="section-desc">Połącz konta zewnętrznych serwisów, aby wzbogacić audyty o dodatkowe dane. Każdy serwis wymaga osobnej autoryzacji.</p>
|
|
|
|
<div class="oauth-cards">
|
|
<!-- Google Business Profile -->
|
|
{% set gbp_conn = connections.get('google/gbp', {}) %}
|
|
<div class="oauth-card" data-provider="google" data-service="gbp">
|
|
<div class="oauth-card-header">
|
|
<div class="oauth-card-icon google">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
|
</div>
|
|
<h3 class="oauth-card-title">Google Business Profile</h3>
|
|
</div>
|
|
<p class="oauth-card-desc">Opinie z odpowiedziami właściciela, posty, zdjęcia, Q&A i statystyki widoczności wizytówki Google.</p>
|
|
|
|
{% if gbp_conn.get('connected') %}
|
|
{% if gbp_conn.get('is_expired') %}
|
|
<div class="oauth-status expired">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
{% if gbp_conn.get('refresh_failed') %}
|
|
Automatyczne odświeżenie nie powiodło się — połącz ponownie
|
|
{% else %}
|
|
Token wygasł — wymagane ponowne połączenie
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status connected">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
Połączono
|
|
</div>
|
|
{% endif %}
|
|
{% if gbp_conn.get('account_name') %}
|
|
<p class="oauth-account-name">Konto: {{ gbp_conn.account_name }}</p>
|
|
{% endif %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth discover" onclick="discoverGBPLocations()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
Wykryj lokalizacje
|
|
</button>
|
|
<button class="btn-oauth disconnect" onclick="disconnectOAuth('google', 'gbp')">
|
|
<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
|
|
</button>
|
|
</div>
|
|
{% elif oauth_available.get('google') %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth connect" onclick="connectOAuth('google', 'gbp')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
|
Połącz konto
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status unavailable">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
|
|
Niedostępne — wymaga konfiguracji administratora
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Google Search Console -->
|
|
{% set sc_conn = connections.get('google/search_console', {}) %}
|
|
<div class="oauth-card" data-provider="google" data-service="search_console">
|
|
<div class="oauth-card-header">
|
|
<div class="oauth-card-icon google">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
</div>
|
|
<h3 class="oauth-card-title">Google Search Console</h3>
|
|
</div>
|
|
<p class="oauth-card-desc">Zapytania wyszukiwania, CTR, średnia pozycja, indeksowanie stron i dane o wydajności w Google.</p>
|
|
|
|
{% if sc_conn.get('connected') %}
|
|
{% if sc_conn.get('is_expired') %}
|
|
<div class="oauth-status expired">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
{% if sc_conn.get('refresh_failed') %}
|
|
Automatyczne odświeżenie nie powiodło się — połącz ponownie
|
|
{% else %}
|
|
Token wygasł — wymagane ponowne połączenie
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status connected">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
Połączono
|
|
</div>
|
|
{% endif %}
|
|
{% if sc_conn.get('account_name') %}
|
|
<p class="oauth-account-name">Konto: {{ sc_conn.account_name }}</p>
|
|
{% endif %}
|
|
<div style="margin: 8px 0; padding: 10px 12px; background: #eff6ff; border-radius: 8px; font-size: 12px; color: #1e40af; line-height: 1.6;">
|
|
<strong>Wazne:</strong> Twoja strona musi byc dodana i zweryfikowana w
|
|
<a href="https://search.google.com/search-console" target="_blank" rel="noopener" style="color: #2563eb; font-weight: 600;">Google Search Console</a>.
|
|
Po weryfikacji dane pojawia sie w audycie SEO po 2-3 dniach.
|
|
</div>
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth disconnect" onclick="disconnectOAuth('google', 'search_console')">
|
|
<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
|
|
</button>
|
|
</div>
|
|
{% elif oauth_available.get('google') %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth connect" onclick="connectOAuth('google', 'search_console')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
|
Połącz konto
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status unavailable">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
|
|
Niedostępne — wymaga konfiguracji administratora
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Facebook -->
|
|
{% set fb_conn = connections.get('meta/facebook', {}) %}
|
|
<div class="oauth-card" data-provider="meta" data-service="facebook">
|
|
<div class="oauth-card-header">
|
|
<div class="oauth-card-icon meta">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><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>
|
|
<h3 class="oauth-card-title">Facebook</h3>
|
|
</div>
|
|
<p class="oauth-card-desc">Zasięg strony, impressions, engagement, dane demograficzne odbiorców i statystyki postów.</p>
|
|
|
|
{% if fb_conn.get('connected') %}
|
|
{% if fb_conn.get('is_expired') %}
|
|
<div class="oauth-status expired">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
{% if fb_conn.get('refresh_failed') %}
|
|
Automatyczne odświeżenie nie powiodło się — połącz ponownie
|
|
{% else %}
|
|
Token wygasł — wymagane ponowne połączenie
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status connected">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
Połączono
|
|
</div>
|
|
{% endif %}
|
|
{% if fb_conn.get('account_name') %}
|
|
<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
|
|
</button>
|
|
</div>
|
|
<details class="fb-connection-help" style="margin-top: 10px;">
|
|
<summary style="cursor: pointer; font-size: 0.82rem; color: var(--text-secondary); user-select: none;">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 4px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
Kiedy połączenie może przestać działać?
|
|
</summary>
|
|
<div style="margin-top: 8px; padding: 10px 12px; background: var(--background); border-radius: var(--radius); font-size: 0.8rem; color: var(--text-secondary); line-height: 1.5;">
|
|
<p style="margin: 0 0 8px 0;"><strong>Połączenie może wymagać odnowienia, gdy:</strong></p>
|
|
<ul style="margin: 0 0 8px 0; padding-left: 18px;">
|
|
<li>Zmienisz hasło do konta Facebook</li>
|
|
<li>Usuniesz aplikację NordaBiz z ustawień Facebooka (Ustawienia → Aplikacje i strony)</li>
|
|
<li>Stracisz rolę administratora strony na Facebooku</li>
|
|
<li>Facebook zmieni zasady dostępu do API</li>
|
|
</ul>
|
|
<p style="margin: 0;"><strong>Co zrobić:</strong> Kliknij <em>Rozłącz</em>, następnie <em>Połącz konto</em> i ponownie wybierz stronę Facebook. Publikowanie postów i statystyki wrócą do normy.</p>
|
|
</div>
|
|
</details>
|
|
{% elif oauth_available.get('meta') %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth connect" onclick="connectOAuth('meta', 'facebook')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
|
Połącz konto
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status unavailable">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
|
|
Niedostępne — wymaga konfiguracji administratora
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Instagram -->
|
|
{% set ig_conn = connections.get('meta/instagram', {}) %}
|
|
<div class="oauth-card" data-provider="meta" data-service="instagram">
|
|
<div class="oauth-card-header">
|
|
<div class="oauth-card-icon meta">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><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>
|
|
<h3 class="oauth-card-title">Instagram</h3>
|
|
</div>
|
|
<p class="oauth-card-desc">Stories, reels, engagement, zasięg postów i dane demograficzne obserwujących konto firmowe.</p>
|
|
|
|
{% if ig_conn.get('connected') %}
|
|
{% if ig_conn.get('is_expired') %}
|
|
<div class="oauth-status expired">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
{% if ig_conn.get('refresh_failed') %}
|
|
Automatyczne odświeżenie nie powiodło się — połącz ponownie
|
|
{% else %}
|
|
Token wygasł — wymagane ponowne połączenie
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status connected">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
Połączono
|
|
</div>
|
|
{% endif %}
|
|
{% if ig_conn.get('account_name') %}
|
|
<p class="oauth-account-name">Konto: {{ ig_conn.account_name }}</p>
|
|
{% endif %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth disconnect" onclick="disconnectOAuth('meta', 'instagram')">
|
|
<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
|
|
</button>
|
|
</div>
|
|
{% elif oauth_available.get('meta') %}
|
|
<div class="oauth-card-actions">
|
|
<button class="btn-oauth connect" onclick="connectOAuth('meta', 'instagram')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
|
Połącz konto
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="oauth-status unavailable">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
|
|
Niedostępne — wymaga konfiguracji administratora
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<a href="{{ url_for('admin.admin_company_get', company_id=company.id) }}" class="back-link">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
|
Powrót do szczegółów firmy
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Disconnect Confirm Modal -->
|
|
<div id="disconnectModal" class="disconnect-modal">
|
|
<div class="disconnect-modal-content">
|
|
<div class="disconnect-modal-icon">
|
|
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="disconnect-modal-title">Rozłączyć serwis?</div>
|
|
<div class="disconnect-modal-desc">Po rozłączeniu audyty nie będą mogły pobierać rozszerzonych danych z tego serwisu. Możesz połączyć go ponownie w dowolnym momencie.</div>
|
|
<div class="disconnect-modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeDisconnectModal()">Anuluj</button>
|
|
<button id="disconnectConfirmBtn" class="btn btn-danger" onclick="executeDisconnect()">Rozłącz</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// OAuth Settings JS
|
|
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
|
|
function showToast(message, type) {
|
|
var existing = document.querySelector('.toast');
|
|
if (existing) existing.remove();
|
|
|
|
var toast = document.createElement('div');
|
|
toast.className = 'toast ' + (type || 'success');
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(function() {
|
|
toast.style.opacity = '0';
|
|
toast.style.transition = 'opacity 0.3s';
|
|
setTimeout(function() { toast.remove(); }, 300);
|
|
}, 4000);
|
|
}
|
|
|
|
function connectOAuth(provider, service) {
|
|
fetch('/api/oauth/connect/' + provider + '/' + service, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.auth_url) {
|
|
window.location.href = data.auth_url;
|
|
} else {
|
|
showToast(data.error || 'Nie udało się rozpocząć autoryzacji', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
showToast('Błąd połączenia: ' + err.message, 'error');
|
|
});
|
|
}
|
|
|
|
var pendingDisconnect = null;
|
|
|
|
function disconnectOAuth(provider, service) {
|
|
pendingDisconnect = { provider: provider, service: service };
|
|
document.getElementById('disconnectModal').classList.add('active');
|
|
return;
|
|
}
|
|
|
|
function closeDisconnectModal() {
|
|
document.getElementById('disconnectModal').classList.remove('active');
|
|
pendingDisconnect = null;
|
|
}
|
|
|
|
function executeDisconnect() {
|
|
if (!pendingDisconnect) return;
|
|
var provider = pendingDisconnect.provider;
|
|
var service = pendingDisconnect.service;
|
|
closeDisconnectModal();
|
|
|
|
fetch('/api/oauth/disconnect/' + provider + '/' + service, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showToast('Serwis rozłączony pomyślnie');
|
|
setTimeout(function() { location.reload(); }, 1000);
|
|
} else {
|
|
showToast(data.error || 'Nie udało się rozłączyć', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
showToast('Błąd: ' + err.message, 'error');
|
|
});
|
|
}
|
|
|
|
function discoverGBPLocations() {
|
|
var btn = document.querySelector('.btn-oauth.discover');
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<svg class="spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;animation:spin 1s linear infinite"><circle cx="12" cy="12" r="10" opacity="0.3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg> Szukam...';
|
|
}
|
|
|
|
fetch('/api/oauth/google/discover-locations', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
var count = data.locations ? data.locations.length : 0;
|
|
showToast('Znaleziono ' + count + ' lokalizacji GBP');
|
|
} else {
|
|
showToast(data.error || 'Nie udało się wyszukać lokalizacji', 'error');
|
|
}
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Wykryj lokalizacje';
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
showToast('Błąd: ' + err.message, 'error');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Wykryj lokalizacje';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle URL params for OAuth callback results
|
|
(function() {
|
|
var params = new URLSearchParams(window.location.search);
|
|
if (params.get('oauth_success')) {
|
|
showToast('Pomyślnie połączono: ' + params.get('oauth_success'));
|
|
// Clean URL
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
}
|
|
if (params.get('oauth_error')) {
|
|
var errorMap = {
|
|
'missing_params': 'Brak wymaganych parametrów',
|
|
'invalid_state': 'Nieprawidłowy token sesji',
|
|
'invalid_state_format': 'Nieprawidłowy format tokenu',
|
|
'unauthorized': 'Brak uprawnień',
|
|
'token_exchange_failed': 'Wymiana tokenu nie powiodła się',
|
|
'save_failed': 'Nie udało się zapisać tokenu'
|
|
};
|
|
var error = params.get('oauth_error');
|
|
showToast(errorMap[error] || 'Błąd OAuth: ' + error, 'error');
|
|
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 %}
|