feat: improve SEO dashboard with problem tags and better UX
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

- Add "Problemy" column showing actionable issues per company
  (Slabe SEO, Wolna strona, Niska dostepnosc, Brak standardow)
- Add tooltips to column headers explaining each metric
- Sort by worst scores first (companies needing help on top)
- Unaudited companies always at bottom regardless of sort
- Replace "medium" stat card with total companies count
- Remove API button from header (admin doesn't need raw JSON)
- Add problem/warn/ok/na tag styling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-11 04:47:23 +01:00
parent d8ee8fe7e4
commit bc7b206741

View File

@ -342,6 +342,53 @@
.legend-dot.medium { background: #fef3c7; border: 1px solid #92400e; }
.legend-dot.poor { background: #fee2e2; border: 1px solid #991b1b; }
/* Problem tags */
.problems-cell {
max-width: 160px;
}
.problem-tag {
display: inline-block;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
background: #fee2e2;
color: #991b1b;
margin: 1px 2px;
}
.warn-tag {
display: inline-block;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
background: #fef3c7;
color: #92400e;
}
.ok-tag {
display: inline-block;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
background: #dcfce7;
color: #166534;
}
.na-tag {
display: inline-block;
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
background: var(--border);
color: var(--text-secondary);
font-style: italic;
}
/* Responsive */
@media (max-width: 1200px) {
.seo-table {
@ -552,36 +599,31 @@
</div>
</div>
<div class="header-actions">
<a href="{{ url_for('api.api_seo_audit') }}" class="btn btn-outline btn-sm" target="_blank">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
API
</a>
{% if current_user.can_access_admin_panel() %}
<button class="btn btn-primary btn-sm" onclick="runBatchAudit()" id="batchAuditBtn">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
Uruchom audyt
Uruchom audyt wszystkich
</button>
{% endif %}
</div>
</div>
<!-- Summary Stats -->
{% set total_companies = stats.good_count + stats.medium_count + stats.poor_count + stats.not_audited_count %}
<div class="stats-grid">
<div class="stat-card">
<span class="stat-number green">{{ stats.good_count }}</span>
<span class="stat-label">Wynik 90-100</span>
<span class="stat-number">{{ total_companies }}</span>
<span class="stat-label">Firm ogolnie</span>
</div>
<div class="stat-card">
<span class="stat-number yellow">{{ stats.medium_count }}</span>
<span class="stat-label">Wynik 50-89</span>
<span class="stat-number green">{{ stats.good_count }}</span>
<span class="stat-label">Dobre SEO (90+)</span>
</div>
<div class="stat-card">
<span class="stat-number red">{{ stats.poor_count }}</span>
<span class="stat-label">Wynik 0-49</span>
<span class="stat-label">Slabe SEO (ponizej 50)</span>
</div>
<div class="stat-card">
<span class="stat-number gray">{{ stats.not_audited_count }}</span>
@ -589,7 +631,7 @@
</div>
<div class="stat-card">
<span class="stat-number">{{ stats.avg_score|default('-', true) }}{% if stats.avg_score %}<small>/100</small>{% endif %}</span>
<span class="stat-label">Sredni wynik SEO</span>
<span class="stat-label">Sredni wynik</span>
</div>
</div>
@ -664,20 +706,21 @@
<th data-sort="category" class="hide-mobile">
Kategoria <span class="sort-icon"></span>
</th>
<th data-sort="overall" class="sorted sorted-desc">
Wynik SEO <span class="sort-icon"></span>
<th data-sort="overall" class="sorted sorted-asc" title="Ogolna ocena SEO strony: meta tagi, struktura, indeksowalnosc">
SEO <span class="sort-icon"></span>
</th>
<th data-sort="performance" class="hide-mobile">
<th data-sort="performance" class="hide-mobile" title="Performance — szybkosc ladowania strony, czas do interakcji">
Performance <span class="sort-icon"></span>
</th>
<th data-sort="accessibility" class="hide-mobile">
Dostepnosc <span class="sort-icon"></span>
<th data-sort="accessibility" class="hide-mobile" title="Accessibility — dostepnosc dla osob z niepelnosprawnosciami (kontrast, opisy, nawigacja klawiatura)">
Accessibility <span class="sort-icon"></span>
</th>
<th data-sort="best_practices" class="hide-mobile">
<th data-sort="best_practices" class="hide-mobile" title="Best Practices — bezpieczenstwo (HTTPS), nowoczesne standardy, brak bledow w konsoli">
Best Practices <span class="sort-icon"></span>
</th>
<th>Problemy</th>
<th data-sort="date">
Ostatni audyt <span class="sort-icon"></span>
Audyt <span class="sort-icon"></span>
</th>
<th>Akcje</th>
</tr>
@ -740,6 +783,36 @@
<span class="score-badge score-na">-</span>
{% endif %}
</td>
<td class="problems-cell">
{% if company.seo_score is not none %}
{% set problems = [] %}
{% if company.seo_score is not none and company.seo_score < 50 %}
{% set _ = problems.append('Slabe SEO') %}
{% endif %}
{% if company.performance_score is not none and company.performance_score < 50 %}
{% set _ = problems.append('Wolna strona') %}
{% endif %}
{% if company.accessibility_score is not none and company.accessibility_score < 50 %}
{% set _ = problems.append('Niska dostepnosc') %}
{% endif %}
{% if company.best_practices_score is not none and company.best_practices_score < 50 %}
{% set _ = problems.append('Brak standardow') %}
{% endif %}
{% if problems %}
{% for p in problems %}
<span class="problem-tag">{{ p }}</span>
{% endfor %}
{% elif company.seo_score >= 90 and (company.performance_score is none or company.performance_score >= 90) %}
<span class="ok-tag">Wszystko OK</span>
{% else %}
<span class="warn-tag">Do poprawy</span>
{% endif %}
{% elif not company.website %}
<span class="na-tag">Brak strony WWW</span>
{% else %}
<span class="na-tag">Brak danych</span>
{% endif %}
</td>
<td class="date-cell">
{% if company.seo_audited_at %}
{% set days_ago = (now - company.seo_audited_at).days %}
@ -869,7 +942,7 @@ document.getElementById('infoModal')?.addEventListener('click', (e) => {
});
// Sorting state
let currentSort = { column: 'overall', direction: 'desc' };
let currentSort = { column: 'overall', direction: 'asc' };
// Sort table
function sortTable(column) {
@ -893,7 +966,7 @@ function sortTable(column) {
}
});
// Sort rows
// Sort rows (unaudited companies always at the bottom)
rows.sort((a, b) => {
let aVal, bVal;
@ -907,8 +980,12 @@ function sortTable(column) {
aVal = new Date(a.dataset.date).getTime();
bVal = new Date(b.dataset.date).getTime();
} else {
aVal = parseFloat(a.dataset[column]) || -1;
bVal = parseFloat(b.dataset[column]) || -1;
aVal = parseFloat(a.dataset[column]);
bVal = parseFloat(b.dataset[column]);
// Push unaudited (-1) to the bottom regardless of sort direction
if (aVal < 0 && bVal >= 0) return 1;
if (bVal < 0 && aVal >= 0) return -1;
if (aVal < 0 && bVal < 0) return 0;
}
if (aVal < bVal) return currentSort.direction === 'asc' ? -1 : 1;
@ -925,6 +1002,9 @@ document.querySelectorAll('.seo-table th[data-sort]').forEach(th => {
th.addEventListener('click', () => sortTable(th.dataset.sort));
});
// Initial sort — worst scores first
sortTable('overall');
// Filtering
function applyFilters() {
const category = document.getElementById('filterCategory').value;