nordabiz/templates/admin/krs_audit_dashboard.html
Maciej Pienczyn 110d971dca
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
feat: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS
(57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash
commands, memory files, architecture docs, and deploy procedures.

Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted
155 .strftime() calls across 71 templates so timestamps display
in Polish timezone regardless of server timezone.

Also includes: created_by_id tracking, abort import fix, ICS
calendar fix for missing end times, Pros Poland data cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:41:53 +02:00

1279 lines
43 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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|local_time('%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|local_time('%Y-%m-%d %H:%M') }}">
{{ company.krs_last_audit_at|local_time('%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 %}