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 clickable field coverage bars to filter companies missing specific data - Add quick-action buttons (Registry/SEO/GBP) per company in dashboard table - Add stale data detection (>6 months) with yellow badges - Implement weighted priority score (contacts 34%, audits 17%) - Add data hints in admin company detail showing where to find missing data - Add "Available data" section showing Google Business data ready to apply - Add POST /api/company/<id>/apply-hint endpoint for one-click data fill - Extend website content updater with phone/email extraction (AI + regex) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1036 lines
38 KiB
HTML
1036 lines
38 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Jakość danych - Admin{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.dq-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.dq-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.dq-header p {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.dq-timestamp {
|
|
text-align: right;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* --- Stat Cards --- */
|
|
.dq-stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.dq-stat-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
padding: var(--spacing-lg);
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dq-stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0;
|
|
height: 4px;
|
|
}
|
|
|
|
.dq-stat-card.total::before { background: linear-gradient(90deg, #3b82f6, #8b5cf6); }
|
|
.dq-stat-card.avg::before { background: linear-gradient(90deg, #10b981, #14b8a6); }
|
|
.dq-stat-card.complete::before { background: linear-gradient(90deg, #22c55e, #16a34a); }
|
|
.dq-stat-card.incomplete::before { background: linear-gradient(90deg, #f59e0b, #f97316); }
|
|
|
|
.dq-stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.dq-stat-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* --- Coverage Bars --- */
|
|
.dq-section {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
padding: var(--spacing-xl);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.dq-section-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.dq-bar-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.dq-bar-label {
|
|
width: 140px;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dq-bar-track {
|
|
flex: 1;
|
|
height: 24px;
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.dq-bar-fill {
|
|
height: 100%;
|
|
border-radius: var(--radius);
|
|
transition: width 0.5s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
padding-right: var(--spacing-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
color: white;
|
|
min-width: 40px;
|
|
}
|
|
|
|
.dq-bar-fill.high { background: linear-gradient(90deg, #22c55e, #16a34a); }
|
|
.dq-bar-fill.medium { background: linear-gradient(90deg, #f59e0b, #d97706); }
|
|
.dq-bar-fill.low { background: linear-gradient(90deg, #ef4444, #dc2626); }
|
|
|
|
.dq-bar-count {
|
|
width: 80px;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* --- Distribution --- */
|
|
.dq-dist-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.dq-dist-card {
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
text-align: center;
|
|
}
|
|
|
|
.dq-dist-card.basic { background: #fef2f2; border: 1px solid #fecaca; }
|
|
.dq-dist-card.enhanced { background: #fffbeb; border: 1px solid #fde68a; }
|
|
.dq-dist-card.complete { background: #f0fdf4; border: 1px solid #bbf7d0; }
|
|
|
|
.dq-dist-value {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.dq-dist-card.basic .dq-dist-value { color: #dc2626; }
|
|
.dq-dist-card.enhanced .dq-dist-value { color: #d97706; }
|
|
.dq-dist-card.complete .dq-dist-value { color: #16a34a; }
|
|
|
|
.dq-dist-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
/* --- Score Distribution --- */
|
|
.dq-score-dist {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-lg);
|
|
}
|
|
|
|
.dq-score-bucket {
|
|
text-align: center;
|
|
padding: var(--spacing-md);
|
|
background: var(--background);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.dq-score-bucket-value {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.dq-score-bucket-label {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* --- Companies Table --- */
|
|
.dq-table-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-md);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.dq-filter-select {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.dq-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.dq-table th {
|
|
text-align: left;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 2px solid var(--border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.dq-table th:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.dq-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.dq-table tr:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.dq-score-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.dq-score-badge.high { background: #dcfce7; color: #166534; }
|
|
.dq-score-badge.medium { background: #fef9c3; color: #854d0e; }
|
|
.dq-score-badge.low { background: #fee2e2; color: #991b1b; }
|
|
|
|
.dq-quality-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dq-quality-badge.basic { background: #fee2e2; color: #991b1b; }
|
|
.dq-quality-badge.enhanced { background: #fef9c3; color: #854d0e; }
|
|
.dq-quality-badge.complete { background: #dcfce7; color: #166534; }
|
|
|
|
.dq-field-dots {
|
|
display: flex;
|
|
gap: 3px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.dq-field-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dq-field-dot.filled { background: #22c55e; }
|
|
.dq-field-dot.empty { background: #e5e7eb; }
|
|
|
|
.dq-company-link {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dq-company-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.dq-bulk-bar {
|
|
display: none;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: var(--primary);
|
|
color: white;
|
|
border-radius: var(--radius-lg);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.dq-bulk-bar.active {
|
|
display: flex;
|
|
}
|
|
|
|
.dq-bulk-btn {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
background: white;
|
|
color: var(--primary);
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dq-bulk-btn:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
/* Pagination */
|
|
.dq-pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: var(--spacing-xs);
|
|
margin-top: var(--spacing-lg);
|
|
}
|
|
|
|
.dq-page-btn {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dq-page-btn.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.dq-page-btn:hover:not(.active) {
|
|
background: var(--background);
|
|
}
|
|
|
|
/* Bar row click & active state */
|
|
.dq-bar-row:hover {
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.dq-bar-row.dq-bar-active {
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
box-shadow: inset 3px 0 0 var(--primary);
|
|
}
|
|
|
|
/* Stale data badge */
|
|
.dq-stale-badge {
|
|
background: #fef9c3;
|
|
color: #854d0e;
|
|
font-size: var(--font-size-xs);
|
|
padding: 1px 6px;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
/* Quick action buttons */
|
|
.dq-action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.dq-action-btn:hover:not(:disabled) {
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
background: #eff6ff;
|
|
}
|
|
|
|
.dq-action-btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.dq-action-btn.loading {
|
|
animation: dq-spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes dq-spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.dq-actions-cell {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
/* Field filter reset */
|
|
.dq-field-filter-info {
|
|
display: none;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: #eff6ff;
|
|
border: 1px solid #bfdbfe;
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
color: #1e40af;
|
|
}
|
|
|
|
.dq-field-filter-info.active {
|
|
display: flex;
|
|
}
|
|
|
|
.dq-field-filter-reset {
|
|
margin-left: auto;
|
|
padding: 2px 8px;
|
|
border: 1px solid #93c5fd;
|
|
border-radius: var(--radius);
|
|
background: white;
|
|
color: #2563eb;
|
|
cursor: pointer;
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.dq-field-filter-reset:hover {
|
|
background: #dbeafe;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.dq-bar-label { width: 100px; font-size: var(--font-size-xs); }
|
|
.dq-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.dq-score-dist { grid-template-columns: repeat(2, 1fr); }
|
|
.dq-table { font-size: var(--font-size-xs); }
|
|
.dq-table td, .dq-table th { padding: var(--spacing-xs); }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dq-header">
|
|
<div>
|
|
<h1>Jakość danych firm</h1>
|
|
<p>Przegląd kompletności i jakości danych {{ total }} firm w katalogu</p>
|
|
</div>
|
|
<div class="dq-timestamp">
|
|
Stan na {{ now.strftime('%d.%m.%Y, %H:%M') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stat Cards -->
|
|
<div class="dq-stats-grid">
|
|
<div class="dq-stat-card total">
|
|
<div class="dq-stat-value">{{ total }}</div>
|
|
<div class="dq-stat-label">Firm w katalogu</div>
|
|
</div>
|
|
<div class="dq-stat-card avg">
|
|
<div class="dq-stat-value">{{ avg_score }}%</div>
|
|
<div class="dq-stat-label">Średnia kompletność</div>
|
|
</div>
|
|
<div class="dq-stat-card complete">
|
|
<div class="dq-stat-value">{{ quality_dist.get('complete', 0) }}</div>
|
|
<div class="dq-stat-label">Kompletnych (67%+)</div>
|
|
</div>
|
|
<div class="dq-stat-card incomplete">
|
|
<div class="dq-stat-value">{{ quality_dist.get('basic', 0) }}</div>
|
|
<div class="dq-stat-label">Podstawowych (<34%)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Field Coverage -->
|
|
<div class="dq-section">
|
|
<div class="dq-section-title">Pokrycie danych per pole</div>
|
|
{% for field_name, stats in field_stats.items() %}
|
|
<div class="dq-bar-row" data-field="{{ field_name }}" onclick="filterByField('{{ field_name }}')" style="cursor: pointer;" title="Kliknij aby filtrować firmy bez tego pola">
|
|
<div class="dq-bar-label">{{ field_name }}</div>
|
|
<div class="dq-bar-track">
|
|
<div class="dq-bar-fill {% if stats.pct >= 70 %}high{% elif stats.pct >= 40 %}medium{% else %}low{% endif %}"
|
|
style="width: {{ stats.pct }}%">
|
|
{{ stats.pct }}%
|
|
</div>
|
|
</div>
|
|
<div class="dq-bar-count">{{ stats.count }}/{{ total }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Quality Distribution -->
|
|
<div class="dq-section">
|
|
<div class="dq-section-title">Rozkład jakości danych</div>
|
|
<div class="dq-dist-grid">
|
|
<div class="dq-dist-card basic">
|
|
<div class="dq-dist-value">{{ quality_dist.get('basic', 0) }}</div>
|
|
<div class="dq-dist-label">Podstawowe (<34%)</div>
|
|
</div>
|
|
<div class="dq-dist-card enhanced">
|
|
<div class="dq-dist-value">{{ quality_dist.get('enhanced', 0) }}</div>
|
|
<div class="dq-dist-label">Rozszerzone (34-66%)</div>
|
|
</div>
|
|
<div class="dq-dist-card complete">
|
|
<div class="dq-dist-value">{{ quality_dist.get('complete', 0) }}</div>
|
|
<div class="dq-dist-label">Kompletne (67%+)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dq-score-dist">
|
|
<div class="dq-score-bucket">
|
|
<div class="dq-score-bucket-value">{{ score_dist.get('0-25', 0) }}</div>
|
|
<div class="dq-score-bucket-label">0-25%</div>
|
|
</div>
|
|
<div class="dq-score-bucket">
|
|
<div class="dq-score-bucket-value">{{ score_dist.get('26-50', 0) }}</div>
|
|
<div class="dq-score-bucket-label">26-50%</div>
|
|
</div>
|
|
<div class="dq-score-bucket">
|
|
<div class="dq-score-bucket-value">{{ score_dist.get('51-75', 0) }}</div>
|
|
<div class="dq-score-bucket-label">51-75%</div>
|
|
</div>
|
|
<div class="dq-score-bucket">
|
|
<div class="dq-score-bucket-value">{{ score_dist.get('76-100', 0) }}</div>
|
|
<div class="dq-score-bucket-label">76-100%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Available Data Section -->
|
|
{% if available_data %}
|
|
<div class="dq-section">
|
|
<div class="dq-section-title">Dane gotowe do uzupełnienia ({{ available_data|length }})</div>
|
|
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-md);">
|
|
Poniższe dane zostały znalezione w Google Business Profile, ale nie są jeszcze w profilu firmy.
|
|
</p>
|
|
|
|
<div style="margin-bottom: var(--spacing-md);">
|
|
<button class="dq-bulk-btn" onclick="applyAllAvailableHints()"
|
|
style="background: var(--primary); color: white; padding: var(--spacing-sm) var(--spacing-lg); border-radius: var(--radius);">
|
|
Uzupełnij wszystkie ({{ available_data|length }})
|
|
</button>
|
|
</div>
|
|
|
|
<table class="dq-table" id="availableDataTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Firma</th>
|
|
<th>Pole</th>
|
|
<th>Źródło</th>
|
|
<th>Wartość</th>
|
|
<th style="width: 100px">Akcja</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in available_data %}
|
|
<tr id="avail-row-{{ loop.index }}">
|
|
<td><a href="{{ url_for('admin.admin_company_detail', company_id=item.company_id) }}" class="dq-company-link">{{ item.company_name }}</a></td>
|
|
<td>{{ item.field }}</td>
|
|
<td><span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ item.source }}</span></td>
|
|
<td style="font-size: var(--font-size-sm);">{{ item.value[:50] }}</td>
|
|
<td>
|
|
<button class="hint-apply-btn" onclick="applyAvailableHint({{ item.company_id }}, '{{ item.field }}', '{{ item.value|e }}', 'avail-row-{{ loop.index }}')"
|
|
style="padding: 2px 10px; font-size: var(--font-size-xs); background: var(--primary); color: white; border: none; border-radius: var(--radius); cursor: pointer;">
|
|
Uzupełnij
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Companies Table -->
|
|
<div class="dq-section">
|
|
<div class="dq-section-title">Firmy wg kompletności danych</div>
|
|
|
|
<!-- Bulk action bar -->
|
|
<div class="dq-bulk-bar" id="bulkBar">
|
|
<span id="selectedCount">0</span> zaznaczonych
|
|
<button class="dq-bulk-btn" onclick="openBulkEnrich()">Uzbrój zaznaczone</button>
|
|
<button class="dq-bulk-btn" onclick="clearSelection()" style="background: transparent; color: white; border: 1px solid rgba(255,255,255,0.5);">Odznacz</button>
|
|
</div>
|
|
|
|
<div class="dq-field-filter-info" id="fieldFilterInfo">
|
|
<span>Filtr pola: <strong id="fieldFilterName"></strong> — firmy bez tego pola</span>
|
|
<button class="dq-field-filter-reset" onclick="resetFieldFilter()">Pokaż wszystkie</button>
|
|
</div>
|
|
|
|
<div class="dq-table-controls">
|
|
<div>
|
|
<select class="dq-filter-select" id="qualityFilter" onchange="filterTable()">
|
|
<option value="all">Wszystkie poziomy</option>
|
|
<option value="basic">Podstawowe</option>
|
|
<option value="enhanced">Rozszerzone</option>
|
|
<option value="complete">Kompletne</option>
|
|
</select>
|
|
</div>
|
|
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">
|
|
Pokazano <span id="shownCount">{{ companies_table|length }}</span> z {{ total }} firm
|
|
</div>
|
|
</div>
|
|
|
|
<table class="dq-table" id="companiesTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 30px"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
|
|
<th onclick="sortTable(1)">Firma</th>
|
|
<th onclick="sortTable(2)" style="width: 100px">Score</th>
|
|
<th onclick="sortTable(3)" style="width: 80px">Pola</th>
|
|
<th style="width: 130px">Kompletność</th>
|
|
<th onclick="sortTable(6)" style="width: 100px">Jakość</th>
|
|
<th style="width: 90px">Akcje</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for c in companies_table %}
|
|
<tr data-quality="{{ c.label }}" data-fields='{{ c.fields|tojson }}' data-nip="{{ c.nip }}" data-website="{{ c.website }}" data-id="{{ c.id }}">
|
|
<td><input type="checkbox" class="company-cb" value="{{ c.id }}"></td>
|
|
<td>
|
|
<a href="{{ url_for('admin.admin_company_detail', company_id=c.id) }}" class="dq-company-link">
|
|
{{ c.name }}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<span class="dq-score-badge {% if c.score >= 67 %}high{% elif c.score >= 34 %}medium{% else %}low{% endif %}">
|
|
{{ c.score }}%
|
|
</span>
|
|
{% if c.registry_stale %}
|
|
<span class="dq-stale-badge" title="Dane z rejestru starsze niż 6 mcy">Dane stare</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ c.filled }}/{{ c.total }}</td>
|
|
<td>
|
|
<div class="dq-field-dots" title="{% for fname, fval in c.fields.items() %}{{ fname }}: {{ 'tak' if fval else 'nie' }} {% endfor %}">
|
|
{% for fname, fval in c.fields.items() %}
|
|
<span class="dq-field-dot {{ 'filled' if fval else 'empty' }}" title="{{ fname }}" data-field="{{ fname }}"></span>
|
|
{% endfor %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="dq-quality-badge {{ c.label }}">
|
|
{% if c.label == 'basic' %}Podstawowe{% elif c.label == 'enhanced' %}Rozszerzone{% else %}Kompletne{% endif %}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="dq-actions-cell">
|
|
{% if not c.fields['Dane urzędowe'] and c.nip %}
|
|
<button class="dq-action-btn" title="Pobierz dane z rejestru" onclick="quickAction(this, 'registry', {{ c.id }})">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
</button>
|
|
{% else %}
|
|
<button class="dq-action-btn" disabled title="Rejestr {% if c.fields['Dane urzędowe'] %}wykonany{% else %}brak NIP{% endif %}">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
</button>
|
|
{% endif %}
|
|
{% if not c.fields['Audyt SEO'] and c.website %}
|
|
<button class="dq-action-btn" title="Uruchom audyt SEO" onclick="quickAction(this, 'seo', {{ c.id }})">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
</button>
|
|
{% else %}
|
|
<button class="dq-action-btn" disabled title="SEO {% if c.fields['Audyt SEO'] %}wykonany{% else %}brak strony{% endif %}">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
</button>
|
|
{% endif %}
|
|
{% if not c.fields['Audyt GBP'] %}
|
|
<button class="dq-action-btn" title="Uruchom audyt GBP" onclick="quickAction(this, 'gbp', {{ c.id }})">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
</button>
|
|
{% else %}
|
|
<button class="dq-action-btn" disabled title="GBP wykonany">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Bulk Enrich Modal -->
|
|
<div id="bulkModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999; align-items: center; justify-content: center;">
|
|
<div style="background: var(--surface); border-radius: var(--radius-xl); padding: var(--spacing-xl); max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;">
|
|
<h3 style="margin-bottom: var(--spacing-lg);">Uzbrój zaznaczone firmy</h3>
|
|
<p style="color: var(--text-secondary); margin-bottom: var(--spacing-lg);">
|
|
Wybierz kroki enrichmentu do wykonania dla <strong id="modalCount">0</strong> firm:
|
|
</p>
|
|
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm); margin-bottom: var(--spacing-xl);">
|
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer;">
|
|
<input type="checkbox" id="step-registry" checked> Dane z rejestrów (CEIDG/KRS)
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer;">
|
|
<input type="checkbox" id="step-seo" checked> Audyt SEO
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer;">
|
|
<input type="checkbox" id="step-social" checked> Audyt Social Media
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer;">
|
|
<input type="checkbox" id="step-gbp" checked> Audyt GBP
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer;">
|
|
<input type="checkbox" id="step-logo"> Pobierz logo
|
|
</label>
|
|
</div>
|
|
<div style="display: flex; gap: var(--spacing-md); justify-content: flex-end;">
|
|
<button onclick="closeBulkModal()" style="padding: var(--spacing-sm) var(--spacing-lg); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); cursor: pointer;">Anuluj</button>
|
|
<button onclick="startBulkEnrich()" style="padding: var(--spacing-sm) var(--spacing-lg); border: none; border-radius: var(--radius); background: var(--primary); color: white; font-weight: 600; cursor: pointer;">Rozpocznij</button>
|
|
</div>
|
|
|
|
<!-- Progress section -->
|
|
<div id="bulkProgress" style="display: none; margin-top: var(--spacing-xl); padding-top: var(--spacing-lg); border-top: 1px solid var(--border);">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-sm);">
|
|
<span style="font-weight: 600;">Postęp</span>
|
|
<span id="progressText">0/0</span>
|
|
</div>
|
|
<div style="height: 8px; background: var(--background); border-radius: 4px; overflow: hidden;">
|
|
<div id="progressBar" style="height: 100%; background: var(--primary); border-radius: 4px; transition: width 0.3s; width: 0%;"></div>
|
|
</div>
|
|
<div id="progressLog" style="margin-top: var(--spacing-md); max-height: 200px; overflow-y: auto; font-size: var(--font-size-xs); font-family: monospace; color: var(--text-secondary);"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
// Data Quality Dashboard JS
|
|
|
|
function filterTable() {
|
|
applyFilters();
|
|
}
|
|
|
|
function sortTable(colIdx) {
|
|
var table = document.getElementById('companiesTable');
|
|
var tbody = table.querySelector('tbody');
|
|
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
var asc = table.dataset.sortCol == colIdx && table.dataset.sortDir !== 'asc';
|
|
table.dataset.sortCol = colIdx;
|
|
table.dataset.sortDir = asc ? 'asc' : 'desc';
|
|
|
|
rows.sort(function(a, b) {
|
|
var aVal = a.cells[colIdx].textContent.trim().replace('%', '');
|
|
var bVal = b.cells[colIdx].textContent.trim().replace('%', '');
|
|
var aNum = parseFloat(aVal);
|
|
var bNum = parseFloat(bVal);
|
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
return asc ? aNum - bNum : bNum - aNum;
|
|
}
|
|
return asc ? aVal.localeCompare(bVal, 'pl') : bVal.localeCompare(aVal, 'pl');
|
|
});
|
|
|
|
rows.forEach(function(row) { tbody.appendChild(row); });
|
|
}
|
|
|
|
// Checkbox selection
|
|
function toggleSelectAll() {
|
|
var checked = document.getElementById('selectAll').checked;
|
|
document.querySelectorAll('.company-cb').forEach(function(cb) {
|
|
var row = cb.closest('tr');
|
|
if (row.style.display !== 'none') {
|
|
cb.checked = checked;
|
|
}
|
|
});
|
|
updateBulkBar();
|
|
}
|
|
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.classList.contains('company-cb')) {
|
|
updateBulkBar();
|
|
}
|
|
});
|
|
|
|
function updateBulkBar() {
|
|
var selected = document.querySelectorAll('.company-cb:checked').length;
|
|
var bar = document.getElementById('bulkBar');
|
|
document.getElementById('selectedCount').textContent = selected;
|
|
if (selected > 0) {
|
|
bar.classList.add('active');
|
|
} else {
|
|
bar.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
document.querySelectorAll('.company-cb').forEach(function(cb) { cb.checked = false; });
|
|
document.getElementById('selectAll').checked = false;
|
|
updateBulkBar();
|
|
}
|
|
|
|
// Bulk enrich modal
|
|
function openBulkEnrich() {
|
|
var selected = document.querySelectorAll('.company-cb:checked').length;
|
|
document.getElementById('modalCount').textContent = selected;
|
|
document.getElementById('bulkModal').style.display = 'flex';
|
|
document.getElementById('bulkProgress').style.display = 'none';
|
|
}
|
|
|
|
function closeBulkModal() {
|
|
document.getElementById('bulkModal').style.display = 'none';
|
|
}
|
|
|
|
function startBulkEnrich() {
|
|
var companyIds = [];
|
|
document.querySelectorAll('.company-cb:checked').forEach(function(cb) {
|
|
companyIds.push(parseInt(cb.value));
|
|
});
|
|
|
|
var steps = [];
|
|
if (document.getElementById('step-registry').checked) steps.push('registry');
|
|
if (document.getElementById('step-seo').checked) steps.push('seo');
|
|
if (document.getElementById('step-social').checked) steps.push('social');
|
|
if (document.getElementById('step-gbp').checked) steps.push('gbp');
|
|
if (document.getElementById('step-logo').checked) steps.push('logo');
|
|
|
|
if (companyIds.length === 0 || steps.length === 0) return;
|
|
|
|
document.getElementById('bulkProgress').style.display = 'block';
|
|
document.getElementById('progressText').textContent = '0/' + companyIds.length;
|
|
document.getElementById('progressLog').innerHTML = '';
|
|
|
|
fetch('/admin/data-quality/bulk-enrich', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''},
|
|
body: JSON.stringify({company_ids: companyIds, steps: steps})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.job_id) {
|
|
pollProgress(data.job_id, companyIds.length);
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
document.getElementById('progressLog').innerHTML += '<div style="color: #ef4444;">Błąd: ' + err.message + '</div>';
|
|
});
|
|
}
|
|
|
|
// --- A1: Filter by field ---
|
|
var activeFieldFilter = null;
|
|
|
|
function filterByField(fieldName) {
|
|
// Toggle: if same field clicked again, reset
|
|
if (activeFieldFilter === fieldName) {
|
|
resetFieldFilter();
|
|
return;
|
|
}
|
|
activeFieldFilter = fieldName;
|
|
|
|
// Highlight active bar
|
|
document.querySelectorAll('.dq-bar-row').forEach(function(row) {
|
|
row.classList.toggle('dq-bar-active', row.dataset.field === fieldName);
|
|
});
|
|
|
|
// Show filter info
|
|
document.getElementById('fieldFilterName').textContent = fieldName;
|
|
document.getElementById('fieldFilterInfo').classList.add('active');
|
|
|
|
applyFilters();
|
|
}
|
|
|
|
function resetFieldFilter() {
|
|
activeFieldFilter = null;
|
|
document.querySelectorAll('.dq-bar-row').forEach(function(row) {
|
|
row.classList.remove('dq-bar-active');
|
|
});
|
|
document.getElementById('fieldFilterInfo').classList.remove('active');
|
|
applyFilters();
|
|
}
|
|
|
|
function applyFilters() {
|
|
var qualityFilter = document.getElementById('qualityFilter').value;
|
|
var rows = document.querySelectorAll('#companiesTable tbody tr');
|
|
var shown = 0;
|
|
rows.forEach(function(row) {
|
|
var qualityMatch = (qualityFilter === 'all' || row.dataset.quality === qualityFilter);
|
|
var fieldMatch = true;
|
|
if (activeFieldFilter) {
|
|
try {
|
|
var fields = JSON.parse(row.dataset.fields);
|
|
// Show only companies MISSING this field
|
|
fieldMatch = !fields[activeFieldFilter];
|
|
} catch(e) { fieldMatch = true; }
|
|
}
|
|
if (qualityMatch && fieldMatch) {
|
|
row.style.display = '';
|
|
shown++;
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
document.getElementById('shownCount').textContent = shown;
|
|
}
|
|
|
|
// --- A2: Quick action buttons ---
|
|
function quickAction(btn, type, companyId) {
|
|
if (btn.disabled || btn.classList.contains('loading')) return;
|
|
var originalHTML = btn.innerHTML;
|
|
btn.classList.add('loading');
|
|
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4m-3.93 7.07l-2.83-2.83M7.76 7.76L4.93 4.93"/></svg>';
|
|
var csrf = document.querySelector('meta[name=csrf-token]')?.content || '';
|
|
var url, body;
|
|
if (type === 'registry') {
|
|
url = '/api/company/' + companyId + '/enrich-registry';
|
|
body = null;
|
|
} else if (type === 'seo') {
|
|
url = '/api/seo/audit';
|
|
body = JSON.stringify({company_id: companyId});
|
|
} else if (type === 'gbp') {
|
|
url = '/api/gbp/audit';
|
|
body = JSON.stringify({company_id: companyId});
|
|
}
|
|
|
|
var opts = {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrf}
|
|
};
|
|
if (body) opts.body = body;
|
|
|
|
fetch(url, opts)
|
|
.then(function(r) { return r.json().then(function(d) { return {ok: r.ok, data: d}; }); })
|
|
.then(function(result) {
|
|
btn.classList.remove('loading');
|
|
if (result.ok) {
|
|
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="3"><path d="M5 13l4 4L19 7"/></svg>';
|
|
btn.disabled = true;
|
|
btn.title = 'Wykonano';
|
|
// Update corresponding dot
|
|
var row = btn.closest('tr');
|
|
var fieldName = type === 'registry' ? 'Dane urzędowe' : (type === 'seo' ? 'Audyt SEO' : 'Audyt GBP');
|
|
var dot = row.querySelector('.dq-field-dot[data-field="' + fieldName + '"]');
|
|
if (dot) {
|
|
dot.classList.remove('empty');
|
|
dot.classList.add('filled');
|
|
}
|
|
} else {
|
|
btn.innerHTML = originalHTML;
|
|
btn.title = 'Błąd: ' + (result.data.error || 'nieznany');
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
btn.classList.remove('loading');
|
|
btn.innerHTML = originalHTML;
|
|
btn.title = 'Błąd: ' + err.message;
|
|
});
|
|
}
|
|
|
|
function pollProgress(jobId, total) {
|
|
fetch('/admin/data-quality/bulk-enrich/status?job_id=' + jobId)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var processed = data.processed || 0;
|
|
var pct = Math.round(processed / total * 100);
|
|
document.getElementById('progressBar').style.width = pct + '%';
|
|
document.getElementById('progressText').textContent = processed + '/' + total;
|
|
|
|
if (data.latest_result) {
|
|
var log = document.getElementById('progressLog');
|
|
log.innerHTML += '<div>' + data.latest_result + '</div>';
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
if (data.status === 'running') {
|
|
setTimeout(function() { pollProgress(jobId, total); }, 2000);
|
|
} else {
|
|
document.getElementById('progressLog').innerHTML += '<div style="color: #22c55e; font-weight: 600;">Zakończono!</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyAvailableHint(companyId, field, value, rowId) {
|
|
var btn = event.target;
|
|
btn.disabled = true;
|
|
btn.textContent = '...';
|
|
|
|
fetch('/api/company/' + companyId + '/apply-hint', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''
|
|
},
|
|
body: JSON.stringify({field: field, value: value})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
var row = document.getElementById(rowId);
|
|
if (row) row.style.opacity = '0.3';
|
|
btn.textContent = 'OK';
|
|
btn.style.background = '#22c55e';
|
|
} else {
|
|
btn.textContent = 'Błąd';
|
|
btn.style.background = '#ef4444';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
btn.textContent = 'Błąd';
|
|
btn.style.background = '#ef4444';
|
|
});
|
|
}
|
|
|
|
function applyAllAvailableHints() {
|
|
if (!confirm('Uzupełnić wszystkie dane z Google Business?')) return;
|
|
var rows = document.querySelectorAll('#availableDataTable tbody tr');
|
|
rows.forEach(function(row) {
|
|
var btn = row.querySelector('.hint-apply-btn');
|
|
if (btn && !btn.disabled) btn.click();
|
|
});
|
|
}
|
|
{% endblock %}
|