feat: Add sortable columns and PKD display in KRS Audit panel

- All columns now sortable (click header to sort asc/desc)
- PKD codes displayed with primary code highlighted (★)
- Show first 2 PKD codes, click '+N more' for tooltip with all
- Backend returns full PKD codes list instead of just count

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-13 17:28:09 +01:00
parent bd23238c1a
commit 4e4b3f4ef0
2 changed files with 280 additions and 13 deletions

12
app.py
View File

@ -8619,10 +8619,11 @@ def admin_krs_audit():
KRSAudit.company_id == company.id
).order_by(KRSAudit.audit_date.desc()).first()
# Get PKD codes count
pkd_count = db.query(CompanyPKD).filter(
# Get PKD codes (all)
pkd_codes = db.query(CompanyPKD).filter(
CompanyPKD.company_id == company.id
).count()
).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all()
pkd_count = len(pkd_codes)
# Get people count
people_count = db.query(CompanyPerson).filter(
@ -8640,6 +8641,11 @@ def admin_krs_audit():
'krs_pdf_path': company.krs_pdf_path,
'audit': latest_audit,
'pkd_count': pkd_count,
'pkd_codes': [{
'code': pkd.pkd_code,
'description': pkd.pkd_description,
'is_primary': pkd.is_primary
} for pkd in pkd_codes],
'people_count': people_count,
'capital_shares_count': company.capital_shares_count
})

View File

@ -217,6 +217,33 @@
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);
}
@ -301,6 +328,93 @@
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;
@ -529,13 +643,13 @@
<table class="krs-table" id="krsTable">
<thead>
<tr>
<th>Firma</th>
<th>KRS</th>
<th class="hide-mobile">Kapital</th>
<th class="hide-mobile">Zarzad</th>
<th class="hide-mobile">PKD</th>
<th>Status</th>
<th>Ostatni audyt</th>
<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>
@ -544,7 +658,11 @@
<tr data-company-id="{{ company.id }}"
data-name="{{ company.name|lower }}"
data-krs="{{ company.krs }}"
data-status="{{ 'audited' if company.krs_last_audit_at else 'pending' }}">
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>
@ -566,8 +684,19 @@
{% endif %}
</td>
<td class="data-cell hide-mobile">
{% if company.pkd_count > 0 %}
<span class="data-value">{{ company.pkd_count }}</span>
{% 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 %}
@ -680,6 +809,138 @@
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) {