- Created blueprints/admin/routes_krs_api.py with 3 routes: - /admin/krs-api/audit (POST) - /admin/krs-api/audit/batch (POST) - /admin/krs-api/pdf/<company_id> - Updated templates to use new URL paths - Added endpoint aliases for backward compatibility - Removed ~420 lines from app.py (8150 -> 7729) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1279 lines
43 KiB
HTML
1279 lines
43 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Panel Audyt KRS - Norda Biznes Partner{% 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;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
align-items: center;
|
||
}
|
||
|
||
/* Summary Cards */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 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-number.blue { color: var(--primary); }
|
||
|
||
.stat-label {
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
/* Progress Section */
|
||
.progress-section {
|
||
background: white;
|
||
padding: var(--spacing-lg);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
margin-bottom: var(--spacing-xl);
|
||
display: none;
|
||
}
|
||
|
||
.progress-section.active {
|
||
display: block;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.progress-title {
|
||
font-size: var(--font-size-lg);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.progress-bar-container {
|
||
height: 24px;
|
||
background: var(--border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
margin-bottom: var(--spacing-sm);
|
||
}
|
||
|
||
.progress-bar-fill {
|
||
height: 100%;
|
||
background: var(--primary);
|
||
border-radius: 12px;
|
||
transition: width 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.progress-message {
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.progress-log {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
font-family: monospace;
|
||
font-size: var(--font-size-sm);
|
||
background: var(--background);
|
||
padding: var(--spacing-md);
|
||
border-radius: var(--radius);
|
||
margin-top: var(--spacing-md);
|
||
}
|
||
|
||
.progress-log-entry {
|
||
padding: var(--spacing-xs) 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.progress-log-entry.success { color: var(--success); font-weight: 600; }
|
||
.progress-log-entry.error { color: var(--error); font-weight: 600; }
|
||
.progress-log-entry.skipped { color: var(--warning, #f59e0b); }
|
||
.progress-log-entry.detail { color: var(--text-secondary); font-size: 0.9em; padding-left: 1em; border-bottom: none; }
|
||
.progress-log-entry.info { color: var(--text-secondary); font-style: italic; }
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* Table Container */
|
||
.table-container {
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.krs-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.krs-table th,
|
||
.krs-table td {
|
||
padding: var(--spacing-md);
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.krs-table th {
|
||
background: var(--background);
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-size-sm);
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.krs-table th.sortable {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.krs-table th.sortable:hover {
|
||
background: var(--border);
|
||
}
|
||
|
||
.krs-table th.sortable::after {
|
||
content: '⇅';
|
||
margin-left: 6px;
|
||
opacity: 0.4;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.krs-table th.sortable.sort-asc::after {
|
||
content: '↑';
|
||
opacity: 1;
|
||
}
|
||
|
||
.krs-table th.sortable.sort-desc::after {
|
||
content: '↓';
|
||
opacity: 1;
|
||
}
|
||
|
||
.krs-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);
|
||
}
|
||
|
||
.krs-number {
|
||
font-family: monospace;
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Status badges */
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--spacing-xs);
|
||
padding: 4px 10px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-badge.audited {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
.status-badge.pending {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
}
|
||
|
||
.status-badge.error {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
/* Data cell */
|
||
.data-cell {
|
||
text-align: center;
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
.data-value {
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.data-label {
|
||
font-size: 10px;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.capital-value {
|
||
font-family: monospace;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Date cell */
|
||
.date-cell {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.date-never {
|
||
color: var(--error);
|
||
font-style: italic;
|
||
}
|
||
|
||
/* PKD display */
|
||
.pkd-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.pkd-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-family: monospace;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pkd-badge.primary {
|
||
background: var(--primary-light, #dbeafe);
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pkd-badge.secondary {
|
||
background: var(--background);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.pkd-more {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.pkd-more:hover {
|
||
color: var(--primary);
|
||
}
|
||
|
||
.pkd-tooltip {
|
||
position: absolute;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: var(--spacing-sm);
|
||
box-shadow: var(--shadow-lg);
|
||
z-index: 100;
|
||
max-width: 350px;
|
||
display: none;
|
||
}
|
||
|
||
.pkd-tooltip.active {
|
||
display: block;
|
||
}
|
||
|
||
.pkd-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.pkd-item {
|
||
font-size: 12px;
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.pkd-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.pkd-item .pkd-code {
|
||
font-family: monospace;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pkd-item.primary .pkd-code {
|
||
color: var(--primary);
|
||
}
|
||
|
||
.pkd-item .pkd-desc {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
|
||
.btn-icon.pdf {
|
||
color: var(--error);
|
||
}
|
||
|
||
.btn-icon.pdf:hover {
|
||
background: #fee2e2;
|
||
border-color: var(--error);
|
||
}
|
||
|
||
.btn-icon:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Modal */
|
||
.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: 500px;
|
||
width: 90%;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.modal-icon.warning {
|
||
background: #fef3c7;
|
||
color: #d97706;
|
||
}
|
||
|
||
.modal-icon.success {
|
||
background: #dcfce7;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: var(--font-size-xl);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-body {
|
||
color: var(--text-secondary);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1200px) {
|
||
.krs-table {
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
.hide-mobile {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="admin-header">
|
||
<div>
|
||
<h1>Panel Audyt KRS</h1>
|
||
<p class="text-muted">Ekstrakcja danych z odpisow KRS (Krajowy Rejestr Sadowy)</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://ekrs.ms.gov.pl/" target="_blank" rel="noopener">eKRS (ekrs.ms.gov.pl)</a></span>
|
||
</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
{% if krs_audit_available %}
|
||
<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 wszystkich
|
||
</button>
|
||
{% else %}
|
||
<span class="text-muted">Usluga audytu niedostepna</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Summary Stats -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<span class="stat-number blue">{{ stats.total_with_krs }}</span>
|
||
<span class="stat-label">Firm z KRS</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number green">{{ stats.audited_count }}</span>
|
||
<span class="stat-label">Przeaudytowane</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number yellow">{{ stats.not_audited_count }}</span>
|
||
<span class="stat-label">Oczekujace</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number gray">{{ stats.no_krs_count }}</span>
|
||
<span class="stat-label">Bez KRS (JDG)</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number">{{ stats.with_capital }}</span>
|
||
<span class="stat-label">Z kapitalem</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number">{{ stats.with_people }}</span>
|
||
<span class="stat-label">Z zarzadem</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-number">{{ stats.with_pkd }}</span>
|
||
<span class="stat-label">Z PKD</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress Section (hidden by default) -->
|
||
<div class="progress-section" id="progressSection">
|
||
<div class="progress-header">
|
||
<span class="progress-title">Audyt w toku...</span>
|
||
<button class="btn btn-sm btn-outline" onclick="cancelAudit()">Anuluj</button>
|
||
</div>
|
||
<div class="progress-bar-container">
|
||
<div class="progress-bar-fill" id="progressBar" style="width: 0%">0%</div>
|
||
</div>
|
||
<div class="progress-message" id="progressMessage">Przygotowywanie...</div>
|
||
<div class="progress-log" id="progressLog"></div>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="filters-bar">
|
||
<div class="filter-group">
|
||
<label for="filterStatus">Status:</label>
|
||
<select id="filterStatus" onchange="applyFilters()">
|
||
<option value="">Wszystkie</option>
|
||
<option value="audited">Przeaudytowane</option>
|
||
<option value="pending">Oczekujace</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label for="filterSearch">Szukaj:</label>
|
||
<input type="text" id="filterSearch" placeholder="Nazwa lub KRS..." 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>
|
||
|
||
<!-- Table -->
|
||
{% if companies %}
|
||
<div class="table-container">
|
||
<table class="krs-table" id="krsTable">
|
||
<thead>
|
||
<tr>
|
||
<th class="sortable" data-sort="name">Firma</th>
|
||
<th class="sortable" data-sort="krs">KRS</th>
|
||
<th class="sortable hide-mobile" data-sort="capital">Kapital</th>
|
||
<th class="sortable hide-mobile" data-sort="zarzad">Zarzad</th>
|
||
<th class="sortable hide-mobile" data-sort="pkd">PKD</th>
|
||
<th class="sortable" data-sort="status">Status</th>
|
||
<th class="sortable" data-sort="date">Ostatni audyt</th>
|
||
<th>Akcje</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="krsTableBody">
|
||
{% for company in companies %}
|
||
<tr data-company-id="{{ company.id }}"
|
||
data-name="{{ company.name|lower }}"
|
||
data-krs="{{ company.krs }}"
|
||
data-capital="{{ company.capital_amount|default(0, true) }}"
|
||
data-zarzad="{{ company.people_count|default(0, true) }}"
|
||
data-pkd="{{ company.pkd_count|default(0, true) }}"
|
||
data-status="{{ 'audited' if company.krs_last_audit_at else 'pending' }}"
|
||
data-date="{{ company.krs_last_audit_at.strftime('%Y%m%d') if company.krs_last_audit_at else '00000000' }}">
|
||
<td class="company-name-cell">
|
||
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
|
||
</td>
|
||
<td>
|
||
<span class="krs-number">{{ company.krs }}</span>
|
||
</td>
|
||
<td class="data-cell hide-mobile">
|
||
{% if company.capital_amount %}
|
||
<span class="capital-value">{{ "{:,.0f}".format(company.capital_amount|float).replace(",", " ") }} PLN</span>
|
||
{% else %}
|
||
<span class="text-muted">-</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="data-cell hide-mobile">
|
||
{% if company.people_count > 0 %}
|
||
<span class="data-value">{{ company.people_count }}</span>
|
||
{% else %}
|
||
<span class="text-muted">-</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="data-cell hide-mobile">
|
||
{% if company.pkd_codes %}
|
||
<div class="pkd-container">
|
||
{% for pkd in company.pkd_codes %}
|
||
{% if pkd.is_primary %}
|
||
<span class="pkd-badge primary" title="{{ pkd.description }}">★ {{ pkd.code }}</span>
|
||
{% elif loop.index <= 2 %}
|
||
<span class="pkd-badge secondary" title="{{ pkd.description }}">{{ pkd.code }}</span>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% if company.pkd_count > 2 %}
|
||
<span class="pkd-more" onclick="showPkdTooltip(this, {{ company.pkd_codes | tojson | safe }})">+{{ company.pkd_count - 2 }} więcej</span>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<span class="text-muted">-</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if company.krs_last_audit_at %}
|
||
<span class="status-badge audited">
|
||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||
</svg>
|
||
OK
|
||
</span>
|
||
{% else %}
|
||
<span class="status-badge pending">
|
||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||
</svg>
|
||
Oczekuje
|
||
</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="date-cell">
|
||
{% if company.krs_last_audit_at %}
|
||
<span title="{{ company.krs_last_audit_at.strftime('%Y-%m-%d %H:%M') }}">
|
||
{{ company.krs_last_audit_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 company.krs_pdf_path %}
|
||
<a href="{{ url_for('api_krs_pdf_download', company_id=company.id) }}" class="btn-icon pdf" title="Pobierz PDF" 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="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||
</svg>
|
||
</a>
|
||
{% endif %}
|
||
{% if krs_audit_available %}
|
||
<button class="btn-icon audit" onclick="runSingleAudit({{ company.id }}, '{{ company.name }}')" title="Uruchom audyt">
|
||
<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">
|
||
<h3>Brak firm z KRS</h3>
|
||
<p>Nie znaleziono firm z numerem KRS do audytu.</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>
|
||
|
||
<!-- Result Modal -->
|
||
<div class="modal" id="resultModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<div class="modal-icon success" id="resultIcon">
|
||
<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="M5 13l4 4L19 7"/>
|
||
</svg>
|
||
</div>
|
||
<div class="modal-title" id="resultTitle">Sukces</div>
|
||
</div>
|
||
<div class="modal-body" id="resultBody">
|
||
Operacja zakonczona pomyslnie.
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-primary" onclick="closeResultModal()">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
const csrfToken = '{{ csrf_token() }}';
|
||
let pendingModalAction = null;
|
||
let auditInProgress = false;
|
||
let currentSort = { column: null, direction: 'asc' };
|
||
let activePkdTooltip = null;
|
||
|
||
// === TABLE SORTING ===
|
||
function initTableSorting() {
|
||
const headers = document.querySelectorAll('.krs-table th.sortable');
|
||
headers.forEach(header => {
|
||
header.addEventListener('click', () => {
|
||
const sortKey = header.dataset.sort;
|
||
sortTable(sortKey, header);
|
||
});
|
||
});
|
||
}
|
||
|
||
function sortTable(sortKey, headerElement) {
|
||
const tbody = document.getElementById('krsTableBody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
|
||
// Determine sort direction
|
||
let direction = 'asc';
|
||
if (currentSort.column === sortKey && currentSort.direction === 'asc') {
|
||
direction = 'desc';
|
||
}
|
||
|
||
// Update header classes
|
||
document.querySelectorAll('.krs-table th.sortable').forEach(th => {
|
||
th.classList.remove('sort-asc', 'sort-desc');
|
||
});
|
||
headerElement.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
||
|
||
// Sort rows
|
||
rows.sort((a, b) => {
|
||
let valA, valB;
|
||
|
||
switch (sortKey) {
|
||
case 'name':
|
||
valA = a.dataset.name || '';
|
||
valB = b.dataset.name || '';
|
||
break;
|
||
case 'krs':
|
||
valA = a.dataset.krs || '';
|
||
valB = b.dataset.krs || '';
|
||
break;
|
||
case 'capital':
|
||
valA = parseFloat(a.dataset.capital) || 0;
|
||
valB = parseFloat(b.dataset.capital) || 0;
|
||
break;
|
||
case 'zarzad':
|
||
valA = parseInt(a.dataset.zarzad) || 0;
|
||
valB = parseInt(b.dataset.zarzad) || 0;
|
||
break;
|
||
case 'pkd':
|
||
valA = parseInt(a.dataset.pkd) || 0;
|
||
valB = parseInt(b.dataset.pkd) || 0;
|
||
break;
|
||
case 'status':
|
||
valA = a.dataset.status === 'audited' ? 1 : 0;
|
||
valB = b.dataset.status === 'audited' ? 1 : 0;
|
||
break;
|
||
case 'date':
|
||
valA = a.dataset.date || '00000000';
|
||
valB = b.dataset.date || '00000000';
|
||
break;
|
||
default:
|
||
valA = '';
|
||
valB = '';
|
||
}
|
||
|
||
// Compare
|
||
if (typeof valA === 'number' && typeof valB === 'number') {
|
||
return direction === 'asc' ? valA - valB : valB - valA;
|
||
} else {
|
||
const comparison = String(valA).localeCompare(String(valB));
|
||
return direction === 'asc' ? comparison : -comparison;
|
||
}
|
||
});
|
||
|
||
// Re-append sorted rows
|
||
rows.forEach(row => tbody.appendChild(row));
|
||
|
||
// Update current sort state
|
||
currentSort = { column: sortKey, direction };
|
||
}
|
||
|
||
// === PKD TOOLTIP ===
|
||
function showPkdTooltip(element, pkdCodes) {
|
||
// Close existing tooltip
|
||
if (activePkdTooltip) {
|
||
activePkdTooltip.remove();
|
||
activePkdTooltip = null;
|
||
}
|
||
|
||
// Create tooltip
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'pkd-tooltip active';
|
||
|
||
let html = '<div class="pkd-list">';
|
||
pkdCodes.forEach(pkd => {
|
||
html += `<div class="pkd-item ${pkd.is_primary ? 'primary' : ''}">
|
||
<span class="pkd-code">${pkd.is_primary ? '★ ' : ''}${pkd.code}</span>
|
||
<div class="pkd-desc">${pkd.description || '-'}</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
tooltip.innerHTML = html;
|
||
|
||
// Position tooltip
|
||
const rect = element.getBoundingClientRect();
|
||
tooltip.style.position = 'fixed';
|
||
tooltip.style.top = (rect.bottom + 5) + 'px';
|
||
tooltip.style.left = rect.left + 'px';
|
||
|
||
// Add to document
|
||
document.body.appendChild(tooltip);
|
||
activePkdTooltip = tooltip;
|
||
|
||
// Close on click outside
|
||
setTimeout(() => {
|
||
document.addEventListener('click', closePkdTooltipOnClickOutside);
|
||
}, 10);
|
||
}
|
||
|
||
function closePkdTooltipOnClickOutside(e) {
|
||
if (activePkdTooltip && !activePkdTooltip.contains(e.target) && !e.target.classList.contains('pkd-more')) {
|
||
activePkdTooltip.remove();
|
||
activePkdTooltip = null;
|
||
document.removeEventListener('click', closePkdTooltipOnClickOutside);
|
||
}
|
||
}
|
||
|
||
// Initialize sorting on page load
|
||
document.addEventListener('DOMContentLoaded', initTableSorting);
|
||
|
||
// Modal functions
|
||
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 showResultModal(title, body, success = true) {
|
||
document.getElementById('resultTitle').textContent = title;
|
||
document.getElementById('resultBody').textContent = body;
|
||
const icon = document.getElementById('resultIcon');
|
||
icon.className = 'modal-icon ' + (success ? 'success' : 'warning');
|
||
document.getElementById('resultModal').classList.add('active');
|
||
}
|
||
|
||
function closeResultModal() {
|
||
document.getElementById('resultModal').classList.remove('active');
|
||
location.reload();
|
||
}
|
||
|
||
// Close modal on backdrop click
|
||
document.getElementById('confirmModal')?.addEventListener('click', (e) => {
|
||
if (e.target.id === 'confirmModal') closeModal();
|
||
});
|
||
document.getElementById('resultModal')?.addEventListener('click', (e) => {
|
||
if (e.target.id === 'resultModal') closeResultModal();
|
||
});
|
||
|
||
// Filter functions
|
||
function applyFilters() {
|
||
const status = document.getElementById('filterStatus').value;
|
||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||
|
||
const rows = document.querySelectorAll('#krsTableBody tr');
|
||
|
||
rows.forEach(row => {
|
||
let show = true;
|
||
|
||
// Status filter
|
||
if (status && row.dataset.status !== status) {
|
||
show = false;
|
||
}
|
||
|
||
// Search filter
|
||
if (search && show) {
|
||
const name = row.dataset.name || '';
|
||
const krs = row.dataset.krs || '';
|
||
if (!name.includes(search) && !krs.includes(search)) {
|
||
show = false;
|
||
}
|
||
}
|
||
|
||
row.style.display = show ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('filterStatus').value = '';
|
||
document.getElementById('filterSearch').value = '';
|
||
applyFilters();
|
||
}
|
||
|
||
// Audit functions
|
||
async function runSingleAudit(companyId, companyName) {
|
||
showModal(
|
||
'Uruchom audyt KRS',
|
||
`Czy chcesz uruchomic audyt KRS dla firmy "${companyName}"?`,
|
||
async () => {
|
||
// Show progress section
|
||
const progressSection = document.getElementById('progressSection');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressMessage = document.getElementById('progressMessage');
|
||
const progressLog = document.getElementById('progressLog');
|
||
|
||
progressSection.classList.add('active');
|
||
progressLog.innerHTML = '';
|
||
|
||
// Stage 1: Searching for PDF
|
||
progressBar.style.width = '10%';
|
||
progressBar.textContent = '10%';
|
||
progressMessage.textContent = `Wyszukiwanie pliku PDF dla ${companyName}...`;
|
||
addLogEntry(progressLog, `Rozpoczynam audyt KRS dla: ${companyName}`, 'info');
|
||
|
||
await sleep(300);
|
||
|
||
// Stage 2: Loading PDF
|
||
progressBar.style.width = '25%';
|
||
progressBar.textContent = '25%';
|
||
progressMessage.textContent = 'Pobieranie danych z pliku PDF...';
|
||
addLogEntry(progressLog, 'Znaleziono plik PDF, wczytuję...', 'info');
|
||
|
||
await sleep(200);
|
||
|
||
// Stage 3: Parsing
|
||
progressBar.style.width = '40%';
|
||
progressBar.textContent = '40%';
|
||
progressMessage.textContent = 'Parsowanie odpisu KRS...';
|
||
addLogEntry(progressLog, 'Ekstrakcja danych z PDF...', 'info');
|
||
|
||
try {
|
||
const response = await fetch('/admin/krs-api/audit', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
},
|
||
body: JSON.stringify({ company_id: companyId })
|
||
});
|
||
|
||
// Stage 4: Processing response
|
||
progressBar.style.width = '80%';
|
||
progressBar.textContent = '80%';
|
||
progressMessage.textContent = 'Zapisywanie danych do bazy...';
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
// Stage 5: Complete
|
||
progressBar.style.width = '100%';
|
||
progressBar.textContent = '100%';
|
||
progressMessage.textContent = 'Audyt zakończony pomyślnie!';
|
||
|
||
addLogEntry(progressLog, `KRS: ${data.data?.krs || '-'}`, 'success');
|
||
addLogEntry(progressLog, `Kapitał: ${data.data?.kapital?.toLocaleString() || '-'} PLN`, 'success');
|
||
addLogEntry(progressLog, `Zarząd: ${data.data?.zarzad_count || 0} osób`, 'success');
|
||
addLogEntry(progressLog, `Wspólnicy: ${data.data?.wspolnicy_count || 0} osób`, 'success');
|
||
addLogEntry(progressLog, `PKD: ${data.data?.pkd_count || 0} kodów`, 'success');
|
||
|
||
await sleep(1500);
|
||
progressSection.classList.remove('active');
|
||
|
||
showResultModal(
|
||
'Audyt zakończony',
|
||
`Pomyślnie wyciągnięto dane dla ${companyName}.\n\n` +
|
||
`Kapitał: ${data.data?.kapital?.toLocaleString() || '-'} PLN\n` +
|
||
`Zarząd: ${data.data?.zarzad_count || 0} osób\n` +
|
||
`Wspólnicy: ${data.data?.wspolnicy_count || 0} osób\n` +
|
||
`PKD: ${data.data?.pkd_count || 0} kodów`,
|
||
true
|
||
);
|
||
|
||
// Refresh page to show updated data
|
||
setTimeout(() => location.reload(), 2000);
|
||
} else {
|
||
progressBar.style.width = '100%';
|
||
progressBar.style.background = '#ef4444';
|
||
progressMessage.textContent = 'Błąd audytu';
|
||
addLogEntry(progressLog, `Błąd: ${data.error || 'Nieznany błąd'}`, 'error');
|
||
|
||
await sleep(1500);
|
||
progressSection.classList.remove('active');
|
||
progressBar.style.background = '';
|
||
|
||
showResultModal('Błąd', data.error || 'Wystąpił nieznany błąd', false);
|
||
}
|
||
} catch (error) {
|
||
progressBar.style.width = '100%';
|
||
progressBar.style.background = '#ef4444';
|
||
progressMessage.textContent = 'Błąd połączenia';
|
||
addLogEntry(progressLog, `Błąd: ${error.message}`, 'error');
|
||
|
||
await sleep(1500);
|
||
progressSection.classList.remove('active');
|
||
progressBar.style.background = '';
|
||
|
||
showResultModal('Błąd połączenia', 'Nie udało się połączyć z serwerem: ' + error.message, false);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
function addLogEntry(logElement, message, type) {
|
||
const entry = document.createElement('div');
|
||
entry.className = 'progress-log-entry ' + type;
|
||
entry.textContent = message;
|
||
logElement.appendChild(entry);
|
||
logElement.scrollTop = logElement.scrollHeight;
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function runBatchAudit() {
|
||
showModal(
|
||
'Uruchom audyt wszystkich firm',
|
||
'Czy chcesz uruchomić audyt KRS dla wszystkich firm z numerem KRS? Każda firma będzie przetwarzana osobno.',
|
||
async () => {
|
||
auditInProgress = true;
|
||
const btn = document.getElementById('batchAuditBtn');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span>Audyt w toku...</span>';
|
||
|
||
const progressSection = document.getElementById('progressSection');
|
||
progressSection.classList.add('active');
|
||
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressMessage = document.getElementById('progressMessage');
|
||
const progressLog = document.getElementById('progressLog');
|
||
|
||
progressBar.style.width = '0%';
|
||
progressBar.textContent = '0%';
|
||
progressMessage.textContent = 'Pobieranie listy firm...';
|
||
progressLog.innerHTML = '';
|
||
|
||
// Get all companies with KRS from the table
|
||
const rows = document.querySelectorAll('table tbody tr[data-company-id]');
|
||
const companies = [];
|
||
rows.forEach(row => {
|
||
const krs = row.dataset.krs;
|
||
if (krs && krs.trim() !== '') {
|
||
companies.push({
|
||
id: parseInt(row.dataset.companyId),
|
||
name: row.querySelector('td:first-child a')?.textContent?.trim() || row.dataset.name || 'Nieznana',
|
||
krs: krs
|
||
});
|
||
}
|
||
});
|
||
|
||
if (companies.length === 0) {
|
||
progressSection.classList.remove('active');
|
||
showResultModal('Brak firm', 'Nie znaleziono firm z numerem KRS do audytu.', false);
|
||
auditInProgress = false;
|
||
btn.disabled = false;
|
||
btn.innerHTML = `
|
||
<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 wszystkich
|
||
`;
|
||
return;
|
||
}
|
||
|
||
addLogEntry(progressLog, `Znaleziono ${companies.length} firm z KRS do audytu`, 'info');
|
||
|
||
let success = 0;
|
||
let failed = 0;
|
||
let skipped = 0;
|
||
|
||
for (let i = 0; i < companies.length; i++) {
|
||
const company = companies[i];
|
||
const percent = Math.round(((i + 1) / companies.length) * 100);
|
||
|
||
progressBar.style.width = `${percent}%`;
|
||
progressBar.textContent = `${percent}%`;
|
||
progressMessage.textContent = `[${i + 1}/${companies.length}] Przetwarzanie: ${company.name}...`;
|
||
|
||
try {
|
||
const response = await fetch('/admin/krs-api/audit', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
},
|
||
body: JSON.stringify({ company_id: company.id })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
success++;
|
||
const d = data.data || {};
|
||
addLogEntry(progressLog, `✓ ${company.name} (KRS: ${company.krs})`, 'success');
|
||
addLogEntry(progressLog, ` 📄 NIP: ${d.nip || '-'}, REGON: ${d.regon || '-'}`, 'detail');
|
||
addLogEntry(progressLog, ` 💰 Kapitał: ${d.kapital?.toLocaleString() || '-'} PLN, Udziały: ${d.liczba_udzialow || '-'}`, 'detail');
|
||
addLogEntry(progressLog, ` 👥 Zarząd: ${d.zarzad_count || 0}, Wspólnicy: ${d.wspolnicy_count || 0}, Prokurenci: ${d.prokurenci_count || 0}`, 'detail');
|
||
addLogEntry(progressLog, ` 🏷️ PKD: ${d.pkd_count || 0} kodów`, 'detail');
|
||
} else {
|
||
if (data.error && data.error.includes('nie ma numeru KRS')) {
|
||
skipped++;
|
||
addLogEntry(progressLog, `⊘ ${company.name} - Brak KRS`, 'skipped');
|
||
} else if (data.error && data.error.includes('Nie znaleziono pliku PDF')) {
|
||
skipped++;
|
||
addLogEntry(progressLog, `⊘ ${company.name} (${company.krs}) - Brak pliku PDF`, 'skipped');
|
||
} else {
|
||
failed++;
|
||
addLogEntry(progressLog, `✗ ${company.name} (${company.krs}) - ${data.error || 'Błąd'}`, 'error');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
failed++;
|
||
addLogEntry(progressLog, `✗ ${company.name} (${company.krs}) - Błąd połączenia`, 'error');
|
||
}
|
||
|
||
// Small delay between requests to not overwhelm the server
|
||
await sleep(100);
|
||
}
|
||
|
||
progressBar.style.width = '100%';
|
||
progressBar.textContent = '100%';
|
||
progressMessage.textContent = `Audyt zakończony! Sukces: ${success}, Błędy: ${failed}, Pominięte: ${skipped}`;
|
||
|
||
addLogEntry(progressLog, `─────────────────────────────`, 'info');
|
||
addLogEntry(progressLog, `PODSUMOWANIE: Sukces: ${success}, Błędy: ${failed}, Pominięte: ${skipped}`, 'info');
|
||
|
||
showResultModal(
|
||
'Audyt zakończony',
|
||
`Przetworzono ${companies.length} firm.\n\nSukces: ${success}\nBłędy: ${failed}\nPominięte: ${skipped}`,
|
||
success > 0
|
||
);
|
||
|
||
auditInProgress = false;
|
||
btn.disabled = false;
|
||
btn.innerHTML = `
|
||
<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 wszystkich
|
||
`;
|
||
|
||
// Refresh page after a delay to show updated data
|
||
setTimeout(() => location.reload(), 3000);
|
||
}
|
||
);
|
||
}
|
||
|
||
function cancelAudit() {
|
||
// Note: Currently can't cancel - just hide progress section
|
||
document.getElementById('progressSection').classList.remove('active');
|
||
}
|
||
{% endblock %}
|