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
Batch audit now collects changes without saving to DB. Admin must review before/after differences and approve or discard. Mirrors the existing social audit enrichment review pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
380 lines
15 KiB
HTML
380 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Przegląd audytu GBP - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.review-page { max-width: 1100px; margin: 0 auto; }
|
|
|
|
.review-header {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
margin-bottom: var(--spacing-xl); flex-wrap: wrap; gap: var(--spacing-md);
|
|
}
|
|
|
|
.review-header h1 { font-size: var(--font-size-2xl); color: var(--text-primary); }
|
|
|
|
.back-link {
|
|
display: inline-flex; align-items: center; gap: var(--spacing-xs);
|
|
color: var(--text-secondary); text-decoration: none; font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
.back-link:hover { color: var(--primary); }
|
|
|
|
.review-summary {
|
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--spacing-md); margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.review-stat {
|
|
background: white; padding: var(--spacing-lg); border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm); text-align: center;
|
|
}
|
|
|
|
.review-stat-value {
|
|
font-size: var(--font-size-2xl); font-weight: 700; display: block;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
.review-stat-value.blue { color: var(--primary); }
|
|
.review-stat-value.green { color: var(--success); }
|
|
.review-stat-value.gray { color: var(--secondary); }
|
|
.review-stat-value.red { color: var(--error); }
|
|
|
|
.review-stat-label { color: var(--text-secondary); font-size: var(--font-size-sm); }
|
|
|
|
/* Approval banner */
|
|
.approval-banner {
|
|
padding: var(--spacing-md) var(--spacing-lg); border-radius: var(--radius-lg);
|
|
margin-bottom: var(--spacing-xl); display: flex; align-items: center; gap: var(--spacing-md);
|
|
}
|
|
.approval-banner.pending { background: #fef3c7; border: 1px solid #fbbf24; color: #92400e; }
|
|
.approval-banner.approved { background: #dcfce7; border: 1px solid #86efac; color: #166534; }
|
|
.approval-banner.empty { background: #f3f4f6; border: 1px solid #d1d5db; color: var(--text-secondary); }
|
|
.approval-banner strong { font-weight: 600; }
|
|
|
|
/* Company cards */
|
|
.company-review {
|
|
background: white; border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
|
|
margin-bottom: var(--spacing-md); overflow: hidden; border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.company-review-header {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: var(--spacing-md) var(--spacing-lg); cursor: pointer; user-select: none;
|
|
}
|
|
.company-review-header:hover { background: #f9fafb; }
|
|
|
|
.company-review-title {
|
|
font-weight: 600; color: var(--text-primary); display: flex;
|
|
align-items: center; gap: var(--spacing-sm);
|
|
}
|
|
|
|
.company-review-meta {
|
|
display: flex; align-items: center; gap: var(--spacing-md);
|
|
font-size: var(--font-size-sm); color: var(--text-secondary);
|
|
}
|
|
|
|
.score-change {
|
|
display: inline-flex; align-items: center; gap: 4px; font-weight: 600;
|
|
}
|
|
.score-change.up { color: var(--success); }
|
|
.score-change.down { color: var(--error); }
|
|
.score-change.same { color: var(--text-muted); }
|
|
.score-change.new { color: var(--primary); }
|
|
|
|
.toggle-arrow { transition: transform 0.2s; }
|
|
.toggle-arrow.open { transform: rotate(180deg); }
|
|
|
|
.company-review-body { padding: 0 var(--spacing-lg) var(--spacing-lg); }
|
|
.company-review-body.hidden { display: none; }
|
|
|
|
.changes-table { width: 100%; border-collapse: collapse; font-size: var(--font-size-sm); }
|
|
.changes-table th {
|
|
text-align: left; padding: var(--spacing-sm) var(--spacing-md);
|
|
background: #f9fafb; color: var(--text-secondary); font-weight: 500;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.changes-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
.old-val { color: var(--error); text-decoration: line-through; }
|
|
.new-val { color: var(--success); font-weight: 600; }
|
|
|
|
.badge {
|
|
display: inline-block; padding: 2px 8px; border-radius: var(--radius-sm);
|
|
font-size: 11px; font-weight: 600;
|
|
}
|
|
.badge.changes { background: #dbeafe; color: #2563eb; }
|
|
.badge.error { background: #fee2e2; color: #dc2626; }
|
|
.badge.no-changes { background: #f3f4f6; color: var(--text-secondary); }
|
|
|
|
/* No changes section */
|
|
.no-changes-section {
|
|
background: white; border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
|
|
padding: var(--spacing-lg); margin-bottom: var(--spacing-xl);
|
|
}
|
|
.no-changes-section h3 { margin-bottom: var(--spacing-md); color: var(--text-secondary); }
|
|
.no-changes-pills {
|
|
display: flex; flex-wrap: wrap; gap: var(--spacing-xs);
|
|
}
|
|
.no-changes-pill {
|
|
padding: 4px 10px; background: #f3f4f6; border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs); color: var(--text-secondary);
|
|
}
|
|
|
|
/* Sticky bottom bar */
|
|
.bottom-action-bar {
|
|
position: sticky; bottom: 0; background: white; border-top: 1px solid var(--border-color);
|
|
padding: var(--spacing-md) var(--spacing-lg); display: flex;
|
|
justify-content: flex-end; gap: var(--spacing-sm); box-shadow: 0 -4px 12px rgba(0,0,0,0.08);
|
|
z-index: 100;
|
|
}
|
|
|
|
/* Confirm modal */
|
|
.modal-overlay {
|
|
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;
|
|
}
|
|
.modal-overlay.active { display: flex; }
|
|
.modal-content {
|
|
background: white; border-radius: var(--radius-lg); padding: var(--spacing-xl);
|
|
max-width: 480px; width: 90%; box-shadow: var(--shadow-lg);
|
|
}
|
|
.modal-content h3 { margin-bottom: var(--spacing-md); }
|
|
.modal-content p { color: var(--text-secondary); margin-bottom: var(--spacing-lg); }
|
|
.modal-buttons { display: flex; gap: var(--spacing-sm); justify-content: flex-end; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="review-page">
|
|
<a href="{{ url_for('admin.admin_gbp_audit') }}" class="back-link">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M15 19l-7-7 7-7"/>
|
|
</svg>
|
|
Powrót do panelu GBP
|
|
</a>
|
|
|
|
<div class="review-header">
|
|
<h1>Przegląd wyników audytu GBP</h1>
|
|
{% if not approved and summary.with_changes > 0 %}
|
|
<div style="display:flex; gap:var(--spacing-sm);">
|
|
<button class="btn btn-outline btn-sm" onclick="discardChanges()">Odrzuć wszystko</button>
|
|
<button class="btn btn-primary btn-sm" id="approveBtn" onclick="approveChanges()">
|
|
Zatwierdź i zapisz ({{ summary.with_changes }})
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Summary -->
|
|
<div class="review-summary">
|
|
<div class="review-stat">
|
|
<span class="review-stat-value blue">{{ summary.total_companies }}</span>
|
|
<span class="review-stat-label">Firm zbadanych</span>
|
|
</div>
|
|
<div class="review-stat">
|
|
<span class="review-stat-value green">{{ summary.with_changes }}</span>
|
|
<span class="review-stat-label">Ze zmianami</span>
|
|
</div>
|
|
<div class="review-stat">
|
|
<span class="review-stat-value gray">{{ summary.without_changes }}</span>
|
|
<span class="review-stat-label">Bez zmian</span>
|
|
</div>
|
|
<div class="review-stat">
|
|
<span class="review-stat-value red">{{ summary.errors }}</span>
|
|
<span class="review-stat-label">Błędy</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Approval banner -->
|
|
{% if approved %}
|
|
<div class="approval-banner approved">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
<strong>Wyniki zostały zatwierdzone i zapisane do bazy danych.</strong>
|
|
</div>
|
|
{% elif summary.with_changes > 0 %}
|
|
<div class="approval-banner pending">
|
|
<svg width="24" height="24" 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-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
|
<div>
|
|
<strong>{{ summary.with_changes }} {{ 'firma ma' if summary.with_changes == 1 else 'firm ma' }} zmiany do zatwierdzenia.</strong>
|
|
Przejrzyj różnice poniżej i zatwierdź lub odrzuć wyniki.
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="approval-banner empty">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
<strong>Brak zmian do zatwierdzenia. Wszystkie audyty są aktualne.</strong>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Companies with changes -->
|
|
{% for entry in pending %}
|
|
<div class="company-review">
|
|
<div class="company-review-header" onclick="toggleCompany(this)">
|
|
<div class="company-review-title">
|
|
{{ entry.company_name }}
|
|
<span class="badge changes">{{ entry.changes|length }} {{ 'zmiana' if entry.changes|length == 1 else 'zmian' }}</span>
|
|
</div>
|
|
<div class="company-review-meta">
|
|
{% if entry.old_score is not none and entry.new_score is not none %}
|
|
{% set diff = entry.new_score - entry.old_score %}
|
|
<span class="score-change {{ 'up' if diff > 0 else ('down' if diff < 0 else 'same') }}">
|
|
{{ entry.old_score }}% → {{ entry.new_score }}%
|
|
{% if diff > 0 %}(+{{ diff }}){% elif diff < 0 %}({{ diff }}){% endif %}
|
|
</span>
|
|
{% elif entry.old_score is none %}
|
|
<span class="score-change new">Nowy: {{ entry.new_score }}%</span>
|
|
{% endif %}
|
|
<svg class="toggle-arrow" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"/></svg>
|
|
</div>
|
|
</div>
|
|
<div class="company-review-body hidden">
|
|
<table class="changes-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Pole</th>
|
|
<th>Poprzednia wartość</th>
|
|
<th>Nowa wartość</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for change in entry.changes %}
|
|
<tr>
|
|
<td>{{ change.label }}</td>
|
|
<td><span class="old-val">{{ change.old }}</span></td>
|
|
<td><span class="new-val">{{ change.new }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<!-- Errors -->
|
|
{% if errors %}
|
|
<h3 style="margin: var(--spacing-xl) 0 var(--spacing-md); color: var(--error);">Błędy ({{ errors|length }})</h3>
|
|
{% for err in errors %}
|
|
<div class="company-review" style="border-left: 3px solid var(--error);">
|
|
<div class="company-review-header" style="cursor:default;">
|
|
<div class="company-review-title">
|
|
{{ err.company_name }}
|
|
<span class="badge error">Błąd</span>
|
|
</div>
|
|
<div class="company-review-meta" style="color:var(--error);">{{ err.error }}</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<!-- No changes section -->
|
|
{% if no_changes %}
|
|
<div class="no-changes-section">
|
|
<h3>Bez zmian ({{ no_changes|length }})</h3>
|
|
<div class="no-changes-pills">
|
|
{% for entry in no_changes %}
|
|
<span class="no-changes-pill">{{ entry.company_name }} ({{ entry.new_score }}%)</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Sticky bottom bar -->
|
|
{% if not approved and summary.with_changes > 0 %}
|
|
<div class="bottom-action-bar">
|
|
<button class="btn btn-outline btn-sm" onclick="discardChanges()">Odrzuć wszystko</button>
|
|
<button class="btn btn-primary btn-sm" id="approveBtn2" onclick="approveChanges()">
|
|
Zatwierdź i zapisz ({{ summary.with_changes }})
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Confirm modal -->
|
|
<div class="modal-overlay" id="confirmModal" onclick="if(event.target===this)closeModal()">
|
|
<div class="modal-content">
|
|
<h3 id="modalTitle"></h3>
|
|
<p id="modalMessage"></p>
|
|
<div class="modal-buttons" id="modalButtons"></div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
function toggleCompany(header) {
|
|
var body = header.nextElementSibling;
|
|
var arrow = header.querySelector('.toggle-arrow');
|
|
body.classList.toggle('hidden');
|
|
arrow.classList.toggle('open');
|
|
}
|
|
|
|
function showModal(title, message, confirmText, confirmClass, onConfirm) {
|
|
document.getElementById('modalTitle').textContent = title;
|
|
document.getElementById('modalMessage').textContent = message;
|
|
var buttons = document.getElementById('modalButtons');
|
|
buttons.innerHTML = '<button class="btn btn-outline btn-sm" onclick="closeModal()">Anuluj</button>' +
|
|
'<button class="btn ' + confirmClass + ' btn-sm" onclick="closeModal();(' + onConfirm.name + ')()">' + confirmText + '</button>';
|
|
document.getElementById('confirmModal').classList.add('active');
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('confirmModal').classList.remove('active');
|
|
}
|
|
|
|
function approveChanges() {
|
|
showModal(
|
|
'Zatwierdź wyniki audytu',
|
|
'Czy na pewno chcesz zapisać {{ summary.with_changes }} wyników audytu GBP do bazy danych? Tej operacji nie można cofnąć.',
|
|
'Zatwierdź i zapisz', 'btn-primary', doApprove
|
|
);
|
|
}
|
|
|
|
function doApprove() {
|
|
var btns = document.querySelectorAll('#approveBtn, #approveBtn2');
|
|
btns.forEach(function(b) { b.disabled = true; b.textContent = 'Zapisywanie...'; });
|
|
|
|
fetch('{{ url_for("admin.admin_gbp_audit_batch_approve") }}', {
|
|
method: 'POST',
|
|
headers: {'X-CSRFToken': '{{ csrf_token() }}'}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.status === 'approved') {
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.error || 'Błąd zatwierdzania');
|
|
btns.forEach(function(b) { b.disabled = false; b.textContent = 'Zatwierdź i zapisz'; });
|
|
}
|
|
})
|
|
.catch(function(e) {
|
|
alert('Błąd połączenia: ' + e.message);
|
|
btns.forEach(function(b) { b.disabled = false; b.textContent = 'Zatwierdź i zapisz'; });
|
|
});
|
|
}
|
|
|
|
function discardChanges() {
|
|
showModal(
|
|
'Odrzuć wyniki',
|
|
'Czy na pewno chcesz odrzucić wszystkie wyniki audytu? Żadne dane nie zostaną zapisane.',
|
|
'Odrzuć', 'btn-outline', doDiscard
|
|
);
|
|
}
|
|
|
|
function doDiscard() {
|
|
fetch('{{ url_for("admin.admin_gbp_audit_batch_discard") }}', {
|
|
method: 'POST',
|
|
headers: {'X-CSRFToken': '{{ csrf_token() }}'}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.status === 'discarded') {
|
|
window.location.href = '{{ url_for("admin.admin_gbp_audit") }}';
|
|
} else {
|
|
alert(data.error || 'Błąd odrzucania');
|
|
}
|
|
})
|
|
.catch(function(e) { alert('Błąd połączenia: ' + e.message); });
|
|
}
|
|
{% endblock %}
|