feat: collapsible sections, reorder layout (charts > app posts > FB posts)
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

Sections now have collapsible headers with localStorage persistence.
Layout order: FB page stats > Analityka (charts) > Top 5 > App posts > FB posts.
Each section can be independently expanded/collapsed by clicking the header.
Collapse state persists across page loads via localStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-20 09:49:42 +01:00
parent c42643b07c
commit 5c93bfa635

View File

@ -92,6 +92,25 @@
box-shadow: var(--shadow);
}
.collapsible-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: var(--spacing-sm);
}
.collapsible-header:hover { background: var(--background); }
.collapsible-header h3 { margin: 0; font-size: var(--font-size-md); }
.collapsible-header .collapse-icon { transition: transform 0.2s; font-size: 1.2em; color: var(--text-secondary); }
.collapsible-header.collapsed .collapse-icon { transform: rotate(-90deg); }
.collapsible-body { transition: max-height 0.3s ease; overflow: hidden; }
.collapsible-body.collapsed { display: none; }
.posts-table {
width: 100%;
border-collapse: collapse;
@ -443,22 +462,19 @@
</div>
{% endfor %}
<!-- Ostatnie posty z Facebook -->
<!-- === SEKCJA 1: Analityka Facebook (wykresy) === -->
{% for company_id_key, fb in fb_stats.items() %}
<div class="fb-posts-section" id="fbPostsSection-{{ company_id_key }}">
<div class="fb-posts-header">
<h3>Posty na Facebook
{% if cached_fb_posts.get(company_id_key) %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;">
({{ cached_fb_posts[company_id_key].total_count }} postów, cache z {{ cached_fb_posts[company_id_key].cached_at.strftime('%d.%m %H:%M') }})
</span>
{% endif %}
</h3>
<div style="display: flex; gap: var(--spacing-xs); align-items: center; flex-wrap: wrap;">
<button class="btn btn-secondary btn-small" onclick="loadFbPosts({{ company_id_key }}, this)">Najnowsze 10</button>
<button class="btn btn-primary btn-small" onclick="refreshAllFbPosts({{ company_id_key }}, this)">Odśwież wszystkie</button>
</div>
</div>
<div class="collapsible-header" onclick="toggleSection('fb-charts-{{ company_id_key }}')">
<h3>Analityka Facebook
{% if cached_fb_posts.get(company_id_key) %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;">
({{ cached_fb_posts[company_id_key].total_count }} postów)
</span>
{% endif %}
</h3>
<span class="collapse-icon">&#9660;</span>
</div>
<div id="fb-charts-{{ company_id_key }}-body" class="collapsible-body">
<div id="fbChartsSection-{{ company_id_key }}" style="display:none;">
<div id="fbChartsInfo-{{ company_id_key }}" style="text-align:center;margin-bottom:var(--spacing-sm);padding:var(--spacing-xs) var(--spacing-sm);color:var(--text-secondary);font-size:var(--font-size-sm);background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);"></div>
<div class="fb-charts-grid">
@ -468,14 +484,28 @@
<div class="fb-chart-card"><h4>Typy postów</h4><div style="height:200px;"><canvas id="postTypesChart-{{ company_id_key }}"></canvas></div></div>
<div class="fb-chart-card"><h4>Najlepszy dzień tygodnia</h4><canvas id="bestDayChart-{{ company_id_key }}"></canvas></div>
<div class="fb-chart-card"><h4>Najlepsza godzina</h4><canvas id="bestHourChart-{{ company_id_key }}"></canvas></div>
<div class="fb-chart-card" style="grid-column: 1 / -1;"><h4>Top 5 postów</h4><div style="height:200px;"><canvas id="topPostsChart-{{ company_id_key }}"></canvas></div></div>
</div>
</div>
<div id="fbPostsContainer-{{ company_id_key }}"></div>
</div>
<div class="collapsible-header" onclick="toggleSection('fb-top5-{{ company_id_key }}')">
<h3>Top 5 postów</h3>
<span class="collapse-icon">&#9660;</span>
</div>
<div id="fb-top5-{{ company_id_key }}-body" class="collapsible-body">
<div id="fbTop5Section-{{ company_id_key }}" style="display:none;">
<div class="fb-chart-card" style="margin-bottom:var(--spacing-md);"><div style="height:200px;"><canvas id="topPostsChart-{{ company_id_key }}"></canvas></div></div>
</div>
</div>
{% endfor %}
{% endif %}
<!-- === SEKCJA 2: Posty tworzone w aplikacji === -->
<div class="collapsible-header" onclick="toggleSection('app-posts')">
<h3>Posty tworzone w aplikacji</h3>
<span class="collapse-icon">&#9660;</span>
</div>
<div id="app-posts-body" class="collapsible-body">
<!-- Filtry -->
<div class="filters-row">
<div class="filter-group">
@ -598,6 +628,33 @@
</div>
{% endif %}
</div>
</div><!-- /app-posts-body -->
<!-- === SEKCJA 3: Posty z Facebooka (zwijalne) === -->
{% if fb_stats %}
{% for company_id_key, fb in fb_stats.items() %}
<div class="fb-posts-section" id="fbPostsSection-{{ company_id_key }}">
<div class="collapsible-header" onclick="toggleSection('fb-posts-{{ company_id_key }}')">
<h3>Posty z Facebooka
{% if cached_fb_posts.get(company_id_key) %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;">
({{ cached_fb_posts[company_id_key].total_count }} postów, cache z {{ cached_fb_posts[company_id_key].cached_at.strftime('%d.%m %H:%M') }})
</span>
{% endif %}
</h3>
<div style="display:flex;gap:var(--spacing-xs);align-items:center;" onclick="event.stopPropagation();">
<button class="btn btn-secondary btn-small" onclick="loadFbPosts({{ company_id_key }}, this)">Najnowsze 10</button>
<button class="btn btn-primary btn-small" onclick="refreshAllFbPosts({{ company_id_key }}, this)">Odśwież wszystkie</button>
</div>
<span class="collapse-icon">&#9660;</span>
</div>
<div id="fb-posts-{{ company_id_key }}-body" class="collapsible-body">
<div id="fbPostsContainer-{{ company_id_key }}"></div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<!-- Confirm Modal -->
@ -728,6 +785,33 @@
}
}
// 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);
@ -1349,8 +1433,10 @@
});
window._fbCharts[companyId].push(chart7);
// Show charts section
// 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) {