Widoczny link do Google PageSpeed Insights (Lighthouse) w nagłówku panelu. Użytkownik/admin wie skąd pochodzą metryki. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
931 lines
29 KiB
HTML
931 lines
29 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Panel SEO - Norda Biznes Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.admin-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.data-source-info {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
margin-top: var(--spacing-sm);
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
background: var(--info-light, #e0f2fe);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--info, #0284c7);
|
|
}
|
|
|
|
.data-source-info svg {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.data-source-info a {
|
|
color: inherit;
|
|
font-weight: 600;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.data-source-info a:hover {
|
|
text-decoration: none;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
align-items: center;
|
|
}
|
|
|
|
/* Summary Cards */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
display: block;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.stat-number.green { color: var(--success); }
|
|
.stat-number.yellow { color: var(--warning); }
|
|
.stat-number.red { color: var(--error); }
|
|
.stat-number.gray { color: var(--secondary); }
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* Filters */
|
|
.filters-bar {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-lg);
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
background: white;
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.filter-group label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.filter-group select,
|
|
.filter-group input {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
min-width: 150px;
|
|
}
|
|
|
|
.filter-group select:focus,
|
|
.filter-group input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* Table Container */
|
|
.table-container {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.seo-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.seo-table th,
|
|
.seo-table td {
|
|
padding: var(--spacing-md);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.seo-table th {
|
|
background: var(--background);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
text-transform: uppercase;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.seo-table th:hover {
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.seo-table th .sort-icon {
|
|
display: inline-block;
|
|
margin-left: var(--spacing-xs);
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.seo-table th.sorted .sort-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
.seo-table th.sorted-asc .sort-icon::after {
|
|
content: '\2191';
|
|
}
|
|
|
|
.seo-table th.sorted-desc .sort-icon::after {
|
|
content: '\2193';
|
|
}
|
|
|
|
.seo-table tbody tr:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.company-name-cell {
|
|
font-weight: 500;
|
|
max-width: 250px;
|
|
}
|
|
|
|
.company-name-cell a {
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.company-name-cell a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.company-website {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
max-width: 200px;
|
|
display: block;
|
|
}
|
|
|
|
/* Score Cells */
|
|
.score-cell {
|
|
text-align: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
.score-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: var(--radius-sm);
|
|
min-width: 45px;
|
|
}
|
|
|
|
.score-good {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.score-medium {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.score-poor {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.score-na {
|
|
background: var(--border);
|
|
color: var(--text-secondary);
|
|
font-style: italic;
|
|
font-weight: normal;
|
|
}
|
|
|
|
/* Overall Score - larger */
|
|
.overall-score {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Date cell */
|
|
.date-cell {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.date-old {
|
|
color: var(--warning);
|
|
}
|
|
|
|
.date-never {
|
|
color: var(--error);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Action buttons */
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-icon:hover {
|
|
background: var(--background);
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.btn-icon.audit {
|
|
color: var(--success);
|
|
}
|
|
|
|
.btn-icon.audit:hover {
|
|
background: #dcfce7;
|
|
border-color: var(--success);
|
|
}
|
|
|
|
/* Category badge */
|
|
.category-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
background: var(--border);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Legend */
|
|
.legend {
|
|
display: flex;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.legend-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.legend-dot.good { background: #dcfce7; border: 1px solid #166534; }
|
|
.legend-dot.medium { background: #fef3c7; border: 1px solid #92400e; }
|
|
.legend-dot.poor { background: #fee2e2; border: 1px solid #991b1b; }
|
|
|
|
/* Responsive */
|
|
@media (max-width: 1200px) {
|
|
.seo-table {
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.hide-mobile {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.filters-bar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.filter-group {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.filter-group select,
|
|
.filter-group input {
|
|
min-width: 100%;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
/* Modal styles */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--surface);
|
|
padding: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
max-width: 480px;
|
|
width: 90%;
|
|
box-shadow: var(--shadow-lg);
|
|
animation: modalSlideIn 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes modalSlideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.modal-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.modal-icon.warning {
|
|
background: #fef3c7;
|
|
color: #d97706;
|
|
}
|
|
|
|
.modal-icon.success {
|
|
background: #dcfce7;
|
|
color: #16a34a;
|
|
}
|
|
|
|
.modal-icon.info {
|
|
background: #dbeafe;
|
|
color: #2563eb;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.modal-body {
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="admin-header">
|
|
<div>
|
|
<h1>Panel SEO</h1>
|
|
<p class="text-muted">Analiza jakosci SEO stron internetowych czlonkow Norda Biznes</p>
|
|
<div class="data-source-info">
|
|
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
<span>Dane z <a href="https://pagespeed.web.dev/" target="_blank" rel="noopener">Google PageSpeed Insights</a> (Lighthouse)</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('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.is_admin %}
|
|
<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
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<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>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number yellow">{{ stats.medium_count }}</span>
|
|
<span class="stat-label">Wynik 50-89</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number red">{{ stats.poor_count }}</span>
|
|
<span class="stat-label">Wynik 0-49</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number gray">{{ stats.not_audited_count }}</span>
|
|
<span class="stat-label">Niezbadane</span>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filters-bar">
|
|
<div class="filter-group">
|
|
<label for="filterCategory">Kategoria:</label>
|
|
<select id="filterCategory" onchange="applyFilters()">
|
|
<option value="">Wszystkie</option>
|
|
{% for category in categories %}
|
|
<option value="{{ category }}">{{ category }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="filterScore">Wynik SEO:</label>
|
|
<select id="filterScore" onchange="applyFilters()">
|
|
<option value="">Wszystkie</option>
|
|
<option value="good">Dobry (90-100)</option>
|
|
<option value="medium">Sredni (50-89)</option>
|
|
<option value="poor">Slaby (0-49)</option>
|
|
<option value="none">Niezbadane</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="filterSearch">Szukaj:</label>
|
|
<input type="text" id="filterSearch" placeholder="Nazwa firmy..." oninput="applyFilters()">
|
|
</div>
|
|
<div class="filter-group" style="margin-left: auto;">
|
|
<button class="btn btn-sm btn-outline" onclick="resetFilters()">Resetuj filtry</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-dot good"></div>
|
|
<span>90-100 (dobry)</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-dot medium"></div>
|
|
<span>50-89 (sredni)</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-dot poor"></div>
|
|
<span>0-49 (slaby)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
{% if companies %}
|
|
<div class="table-container">
|
|
<table class="seo-table" id="seoTable">
|
|
<thead>
|
|
<tr>
|
|
<th data-sort="name">
|
|
Firma <span class="sort-icon"></span>
|
|
</th>
|
|
<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>
|
|
<th data-sort="performance" class="hide-mobile">
|
|
Performance <span class="sort-icon"></span>
|
|
</th>
|
|
<th data-sort="accessibility" class="hide-mobile">
|
|
Dostepnosc <span class="sort-icon"></span>
|
|
</th>
|
|
<th data-sort="best_practices" class="hide-mobile">
|
|
Best Practices <span class="sort-icon"></span>
|
|
</th>
|
|
<th data-sort="date">
|
|
Ostatni audyt <span class="sort-icon"></span>
|
|
</th>
|
|
<th>Akcje</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="seoTableBody">
|
|
{% for company in companies %}
|
|
<tr data-category="{{ company.category }}"
|
|
data-name="{{ company.name|lower }}"
|
|
data-overall="{{ company.seo_score if company.seo_score is not none else -1 }}"
|
|
data-performance="{{ company.performance_score if company.performance_score is not none else -1 }}"
|
|
data-accessibility="{{ company.accessibility_score if company.accessibility_score is not none else -1 }}"
|
|
data-best_practices="{{ company.best_practices_score if company.best_practices_score is not none else -1 }}"
|
|
data-date="{{ company.seo_audited_at.isoformat() if company.seo_audited_at else '1970-01-01' }}">
|
|
<td class="company-name-cell">
|
|
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
|
|
{% if company.website %}
|
|
<span class="company-website" title="{{ company.website }}">{{ company.website }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="hide-mobile">
|
|
<span class="category-badge">{{ company.category or 'Inne' }}</span>
|
|
</td>
|
|
<td class="score-cell">
|
|
{% if company.seo_score is not none %}
|
|
<span class="score-badge overall-score {{ 'score-good' if company.seo_score >= 90 else ('score-medium' if company.seo_score >= 50 else 'score-poor') }}">
|
|
{{ company.seo_score }}
|
|
</span>
|
|
{% else %}
|
|
<span class="score-badge score-na">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="score-cell hide-mobile">
|
|
{% if company.performance_score is not none %}
|
|
<span class="score-badge {{ 'score-good' if company.performance_score >= 90 else ('score-medium' if company.performance_score >= 50 else 'score-poor') }}">
|
|
{{ company.performance_score }}
|
|
</span>
|
|
{% else %}
|
|
<span class="score-badge score-na">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="score-cell hide-mobile">
|
|
{% if company.accessibility_score is not none %}
|
|
<span class="score-badge {{ 'score-good' if company.accessibility_score >= 90 else ('score-medium' if company.accessibility_score >= 50 else 'score-poor') }}">
|
|
{{ company.accessibility_score }}
|
|
</span>
|
|
{% else %}
|
|
<span class="score-badge score-na">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="score-cell hide-mobile">
|
|
{% if company.best_practices_score is not none %}
|
|
<span class="score-badge {{ 'score-good' if company.best_practices_score >= 90 else ('score-medium' if company.best_practices_score >= 50 else 'score-poor') }}">
|
|
{{ company.best_practices_score }}
|
|
</span>
|
|
{% else %}
|
|
<span class="score-badge score-na">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="date-cell">
|
|
{% if company.seo_audited_at %}
|
|
{% set days_ago = (now - company.seo_audited_at).days %}
|
|
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.seo_audited_at.strftime('%Y-%m-%d %H:%M') }}">
|
|
{{ company.seo_audited_at.strftime('%d.%m.%Y') }}
|
|
</span>
|
|
{% else %}
|
|
<span class="date-never">Nigdy</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn-icon" title="Zobacz profil">
|
|
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
</svg>
|
|
</a>
|
|
{% if current_user.is_admin %}
|
|
<button class="btn-icon audit" onclick="runSingleAudit('{{ company.slug }}')" title="Uruchom audyt SEO">
|
|
<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>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" opacity="0.3">
|
|
<circle cx="40" cy="40" r="30" stroke="currentColor" stroke-width="3"/>
|
|
<path d="M30 40h20M40 30v20" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
|
|
</svg>
|
|
<h3>Brak firm do wyswietlenia</h3>
|
|
<p>Nie znaleziono firm z danymi SEO.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Confirmation Modal -->
|
|
<div class="modal" id="confirmModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div class="modal-icon warning">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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="modal-title" id="modalTitle">Potwierdz operacje</div>
|
|
</div>
|
|
<div class="modal-body" id="modalBody">
|
|
Czy na pewno chcesz wykonac te operacje?
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline" onclick="closeModal()">Anuluj</button>
|
|
<button class="btn btn-primary" onclick="confirmModalAction()">Potwierdz</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Modal -->
|
|
<div class="modal" id="infoModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div class="modal-icon info">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="modal-title" id="infoModalTitle">Informacja</div>
|
|
</div>
|
|
<div class="modal-body" id="infoModalBody">
|
|
Tresc informacji.
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-primary" onclick="closeInfoModal()">OK</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
const csrfToken = '{{ csrf_token() }}';
|
|
|
|
// Modal functions
|
|
let pendingModalAction = null;
|
|
|
|
function showModal(title, body, onConfirm) {
|
|
document.getElementById('modalTitle').textContent = title;
|
|
document.getElementById('modalBody').textContent = body;
|
|
pendingModalAction = onConfirm;
|
|
document.getElementById('confirmModal').classList.add('active');
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('confirmModal').classList.remove('active');
|
|
pendingModalAction = null;
|
|
}
|
|
|
|
function confirmModalAction() {
|
|
if (pendingModalAction) {
|
|
pendingModalAction();
|
|
}
|
|
closeModal();
|
|
}
|
|
|
|
function showInfoModal(title, body) {
|
|
document.getElementById('infoModalTitle').textContent = title;
|
|
document.getElementById('infoModalBody').textContent = body;
|
|
document.getElementById('infoModal').classList.add('active');
|
|
}
|
|
|
|
function closeInfoModal() {
|
|
document.getElementById('infoModal').classList.remove('active');
|
|
}
|
|
|
|
// Close modal on backdrop click
|
|
document.getElementById('confirmModal')?.addEventListener('click', (e) => {
|
|
if (e.target.id === 'confirmModal') closeModal();
|
|
});
|
|
document.getElementById('infoModal')?.addEventListener('click', (e) => {
|
|
if (e.target.id === 'infoModal') closeInfoModal();
|
|
});
|
|
|
|
// Sorting state
|
|
let currentSort = { column: 'overall', direction: 'desc' };
|
|
|
|
// Sort table
|
|
function sortTable(column) {
|
|
const tbody = document.getElementById('seoTableBody');
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
const headers = document.querySelectorAll('.seo-table th[data-sort]');
|
|
|
|
// Toggle direction if same column
|
|
if (currentSort.column === column) {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.column = column;
|
|
currentSort.direction = 'desc';
|
|
}
|
|
|
|
// Update header classes
|
|
headers.forEach(h => {
|
|
h.classList.remove('sorted', 'sorted-asc', 'sorted-desc');
|
|
if (h.dataset.sort === column) {
|
|
h.classList.add('sorted', `sorted-${currentSort.direction}`);
|
|
}
|
|
});
|
|
|
|
// Sort rows
|
|
rows.sort((a, b) => {
|
|
let aVal, bVal;
|
|
|
|
if (column === 'name') {
|
|
aVal = a.dataset.name || '';
|
|
bVal = b.dataset.name || '';
|
|
} else if (column === 'category') {
|
|
aVal = a.dataset.category || '';
|
|
bVal = b.dataset.category || '';
|
|
} else if (column === 'date') {
|
|
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;
|
|
}
|
|
|
|
if (aVal < bVal) return currentSort.direction === 'asc' ? -1 : 1;
|
|
if (aVal > bVal) return currentSort.direction === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
// Re-append rows
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
}
|
|
|
|
// Setup sorting click handlers
|
|
document.querySelectorAll('.seo-table th[data-sort]').forEach(th => {
|
|
th.addEventListener('click', () => sortTable(th.dataset.sort));
|
|
});
|
|
|
|
// Filtering
|
|
function applyFilters() {
|
|
const category = document.getElementById('filterCategory').value;
|
|
const score = document.getElementById('filterScore').value;
|
|
const search = document.getElementById('filterSearch').value.toLowerCase();
|
|
|
|
const rows = document.querySelectorAll('#seoTableBody tr');
|
|
|
|
rows.forEach(row => {
|
|
let show = true;
|
|
|
|
// Category filter
|
|
if (category && row.dataset.category !== category) {
|
|
show = false;
|
|
}
|
|
|
|
// Score filter
|
|
if (score && show) {
|
|
const overallScore = parseFloat(row.dataset.overall);
|
|
if (score === 'good' && (overallScore < 90 || overallScore < 0)) show = false;
|
|
else if (score === 'medium' && (overallScore < 50 || overallScore >= 90)) show = false;
|
|
else if (score === 'poor' && (overallScore < 0 || overallScore >= 50)) show = false;
|
|
else if (score === 'none' && overallScore >= 0) show = false;
|
|
}
|
|
|
|
// Search filter
|
|
if (search && show) {
|
|
if (!row.dataset.name.includes(search)) {
|
|
show = false;
|
|
}
|
|
}
|
|
|
|
row.style.display = show ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
function resetFilters() {
|
|
document.getElementById('filterCategory').value = '';
|
|
document.getElementById('filterScore').value = '';
|
|
document.getElementById('filterSearch').value = '';
|
|
applyFilters();
|
|
}
|
|
|
|
// Audit functions
|
|
function runSingleAudit(slug) {
|
|
showModal(
|
|
'Uruchom audyt SEO',
|
|
'Czy na pewno chcesz uruchomić audyt SEO dla tej firmy? Analiza może potrwać kilka minut.',
|
|
async () => {
|
|
try {
|
|
const response = await fetch('/api/seo/audit', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify({ slug: slug })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
showInfoModal('Audyt zakończony', 'Audyt SEO zakończony pomyślnie! Strona zostanie odświeżona.');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showInfoModal('Błąd', 'Wystąpił błąd: ' + (data.error || 'Nieznany błąd'));
|
|
}
|
|
} catch (error) {
|
|
showInfoModal('Błąd połączenia', 'Nie udało się połączyć z serwerem: ' + error.message);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
function runBatchAudit() {
|
|
showModal(
|
|
'Uruchom audyt wsadowy',
|
|
'Czy na pewno chcesz uruchomić audyt SEO dla wszystkich firm? To może potrwać dłuższy czas.',
|
|
() => {
|
|
const btn = document.getElementById('batchAuditBtn');
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span>Audyt w toku...</span>';
|
|
|
|
showInfoModal('Audyt uruchomiony', 'Audyt wsadowy został uruchomiony w tle. Może to potrwać kilkadziesiąt minut. Sprawdź wyniki później.');
|
|
|
|
setTimeout(() => {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}, 2000);
|
|
}
|
|
);
|
|
}
|
|
{% endblock %}
|