Startseite: Bewerbungsliste als klappbare Sektionen (eine Zeile pro Bewerbung)

Tabelle auf Grid-Layout umgebaut. Status-Gruppen und der Detailbereich
(Verlauf/Notizen) je Bewerbung sind animiert zuklappbar, Default zugeklappt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 13:36:29 +02:00
co-authored by Claude Opus 4.8
parent 7cc527b013
commit 82431490a9
+216 -163
View File
@@ -177,27 +177,47 @@
</div> </div>
</div> </div>
<!-- Applications Table --> <!-- Applications -->
<style>
/* Shared column grid used by the header labels and every application line */
.app-grid {
display: grid;
grid-template-columns: 1.25rem 5.5rem minmax(7rem, 1.6fr) minmax(8rem, 2fr) 5.5rem 8.5rem 4.5rem;
gap: 0 0.75rem;
align-items: center;
}
/* Smooth height collapse via the grid-template-rows 0fr → 1fr trick */
.collapsible {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.collapsible > .collapsible-inner {
overflow: hidden;
min-height: 0;
}
.collapsible.open {
grid-template-rows: 1fr;
}
.chevron {
transition: transform 0.25s ease;
}
.chevron.rotate {
transform: rotate(90deg);
}
</style>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-x-auto"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-x-auto">
<table class="w-full min-w-[720px] table-fixed"> <div class="min-w-[760px]">
<colgroup> <!-- Column labels -->
<col class="w-24"> <div class="app-grid px-6 py-3 bg-gray-50 dark:bg-gray-700 text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<col class="w-[22%]"> <span></span>
<col class="w-[28%]"> <span>Datum</span>
<col class="w-28"> <span>Firma</span>
<col class="w-36"> <span>Stelle</span>
<col class="w-24"> <span>Art</span>
</colgroup> <span>Status</span>
<thead class="bg-gray-50 dark:bg-gray-700"> <span>Aktionen</span>
<tr> </div>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Datum</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Firma</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Stelle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Art</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<% if (applications.length > 0) { %> <% if (applications.length > 0) { %>
<% <%
// Status-Reihenfolge synchron zu public/js/main.js halten // Status-Reihenfolge synchron zu public/js/main.js halten
@@ -223,172 +243,205 @@
...Object.keys(groups).filter(k => !statusOrder.includes(k)) ...Object.keys(groups).filter(k => !statusOrder.includes(k))
]; ];
%> %>
<div id="appList">
<% orderedKeys.forEach(statusKey => { %> <% orderedKeys.forEach(statusKey => { %>
<tbody class="status-group bg-gray-100 dark:bg-gray-900/40 border-b border-gray-200 dark:border-gray-700"> <section class="status-group border-t border-gray-200 dark:border-gray-700">
<tr> <!-- Klappbarer Status-Kopf (Gesendet, Eingangsbestätigung, …) -->
<td colspan="6" class="px-6 py-3"> <button type="button" class="group-toggle w-full flex items-center gap-3 px-6 py-3 bg-gray-100 dark:bg-gray-900/40 hover:bg-gray-200/70 dark:hover:bg-gray-900/60 transition-colors text-left">
<div class="flex items-center gap-3"> <svg class="chevron w-4 h-4 flex-shrink-0 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<span class="inline-block w-2.5 h-2.5 rounded-full <%= statusDot[statusKey] || 'bg-gray-300' %>"></span> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
<span class="text-sm font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-200"><%= statusKey %></span> </svg>
<span data-group-count class="text-xs font-medium px-2 py-0.5 rounded-full bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300"><%= groups[statusKey].length %></span> <span class="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 <%= statusDot[statusKey] || 'bg-gray-300' %>"></span>
<span class="text-sm font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-200"><%= statusKey %></span>
<span data-group-count class="text-xs font-medium px-2 py-0.5 rounded-full bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300"><%= groups[statusKey].length %></span>
</button>
<div class="group-body collapsible">
<div class="collapsible-inner">
<% groups[statusKey].forEach(app => { %>
<%
const hatNotizen = app.notizen && app.notizen.trim().length > 0;
const hatInterne = app.interne_notizen && app.interne_notizen.trim().length > 0;
const hatVerlauf = app.verlauf && app.verlauf.length > 0;
const hatDetails = hatNotizen || hatInterne || hatVerlauf;
%>
<div class="app-row border-t border-gray-100 dark:border-gray-700/60 <%= hatDetails ? 'has-details' : '' %>" data-search="<%= ((app.firma || '') + ' ' + (app.stelle || '')).toLowerCase().replace(/"/g, '') %>">
<!-- Eine Zeile pro Bewerbung -->
<div class="app-line app-grid px-6 py-3 text-sm transition-colors <%= hatDetails ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50' : '' %>">
<span class="flex items-center justify-center">
<% if (hatDetails) { %>
<svg class="chevron w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
<% } %>
</span>
<span class="whitespace-nowrap text-gray-900 dark:text-white"><%= new Date(app.datum).toLocaleDateString('de-DE') %></span>
<span class="min-w-0 truncate font-medium text-gray-900 dark:text-white" title="<%= app.firma %>"><%= app.firma %></span>
<span class="min-w-0 truncate text-gray-900 dark:text-white" title="<%= app.stelle %>"><%= app.stelle %></span>
<span class="min-w-0 truncate text-gray-500 dark:text-gray-400"><%= app.art || '-' %></span>
<span class="min-w-0">
<span class="inline-block max-w-full truncate align-middle px-2 py-1 rounded-full text-xs font-medium
<%= app.status === 'Einstellung' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' :
app.status === 'Absage' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
app.status === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' :
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
<%= app.status || '-' %>
</span>
</span>
<span class="app-actions flex items-center gap-3">
<a href="/bewerbung/<%= app.id %>"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
aria-label="Bearbeiten">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</a>
<button type="button"
class="delete-btn text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
data-id="<%= app.id %>" aria-label="Löschen">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</span>
</div>
<% if (hatDetails) { %>
<!-- Klappbarer Detailbereich unter der Zeile -->
<div class="app-details collapsible">
<div class="collapsible-inner">
<div class="px-6 pb-5 pt-1 space-y-3">
<% if (hatVerlauf) { %>
<div class="rounded-lg border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50/60 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-indigo-500 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">Status-Verlauf</span>
</div>
<ol class="space-y-1.5">
<% app.verlauf.forEach(v => { %>
<li class="flex flex-wrap gap-x-2 text-sm">
<span class="text-gray-500 dark:text-gray-400 whitespace-nowrap"><%= new Date(v.datum).toLocaleDateString('de-DE') %></span>
<span class="font-semibold text-gray-800 dark:text-gray-100"><%= v.status %></span>
<% if (v.kommentar && v.kommentar.trim()) { %>
<span class="text-gray-600 dark:text-gray-300 whitespace-pre-wrap break-words"> <%= v.kommentar %></span>
<% } %>
</li>
<% }); %>
</ol>
</div>
<% } %>
<% if (hatNotizen) { %>
<div class="rounded-lg border-l-4 border-blue-400 dark:border-blue-500 bg-blue-50/70 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-400">Notizen</span>
</div>
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.notizen %></p>
</div>
<% } %>
<% if (hatInterne) { %>
<div class="rounded-lg border-l-4 border-amber-400 dark:border-amber-500 bg-amber-50/70 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-amber-500 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">Interne Notizen · nicht im PDF</span>
</div>
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.interne_notizen %></p>
</div>
<% } %>
</div>
</div>
</div>
<% } %>
</div> </div>
</td> <% }); %>
</tr> </div>
</tbody> </div>
<% groups[statusKey].forEach(app => { %> </section>
<%
const hatNotizen = app.notizen && app.notizen.trim().length > 0;
const hatInterne = app.interne_notizen && app.interne_notizen.trim().length > 0;
const hatVerlauf = app.verlauf && app.verlauf.length > 0;
const hatDetails = hatNotizen || hatInterne || hatVerlauf;
const padB = hatDetails ? 'pb-2' : 'pb-4';
%>
<tbody class="app-row bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700" data-search="<%= ((app.firma || '') + ' ' + (app.stelle || '')).toLowerCase().replace(/"/g, '') %>">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm text-gray-900 dark:text-white">
<%= new Date(app.datum).toLocaleDateString('de-DE') %>
</td>
<td class="px-6 pt-4 <%= padB %> align-top text-sm font-medium text-gray-900 dark:text-white break-words">
<%= app.firma %>
</td>
<td class="px-6 pt-4 <%= padB %> align-top text-sm text-gray-900 dark:text-white break-words">
<%= app.stelle %>
</td>
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= app.art || '-' %>
</td>
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm">
<span class="px-2 py-1 rounded-full text-xs font-medium
<%= app.status === 'Einstellung' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' :
app.status === 'Absage' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
app.status === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' :
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
<%= app.status || '-' %>
</span>
</td>
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm align-top">
<a href="/bewerbung/<%= app.id %>"
class="inline-block align-middle text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 mr-3"
aria-label="Bearbeiten">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</a>
<button
class="delete-btn align-middle text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
data-id="<%= app.id %>">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</td>
</tr>
<% if (hatDetails) { %>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td colspan="6" class="px-6 pb-5 pt-0 space-y-3">
<% if (hatVerlauf) { %>
<div class="rounded-lg border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50/60 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-indigo-500 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">Status-Verlauf</span>
</div>
<ol class="space-y-1.5">
<% app.verlauf.forEach(v => { %>
<li class="flex flex-wrap gap-x-2 text-sm">
<span class="text-gray-500 dark:text-gray-400 whitespace-nowrap"><%= new Date(v.datum).toLocaleDateString('de-DE') %></span>
<span class="font-semibold text-gray-800 dark:text-gray-100"><%= v.status %></span>
<% if (v.kommentar && v.kommentar.trim()) { %>
<span class="text-gray-600 dark:text-gray-300 whitespace-pre-wrap break-words"> <%= v.kommentar %></span>
<% } %>
</li>
<% }); %>
</ol>
</div>
<% } %>
<% if (hatNotizen) { %>
<div class="rounded-lg border-l-4 border-blue-400 dark:border-blue-500 bg-blue-50/70 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-400">Notizen</span>
</div>
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.notizen %></p>
</div>
<% } %>
<% if (hatInterne) { %>
<div class="rounded-lg border-l-4 border-amber-400 dark:border-amber-500 bg-amber-50/70 dark:bg-gray-700/40 px-4 py-3">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-amber-500 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<span class="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">Interne Notizen · nicht im PDF</span>
</div>
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.interne_notizen %></p>
</div>
<% } %>
</td>
</tr>
<% } %>
</tbody>
<% }); %>
<% }); %> <% }); %>
<tbody id="searchNoResults" class="bg-white dark:bg-gray-800" style="display:none"> </div>
<tr> <div id="searchNoResults" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700" style="display:none">
<td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400"> <p>Keine Firma gefunden für „<span id="searchNoResultsTerm"></span>".</p>
<p>Keine Firma gefunden für „<span id="searchNoResultsTerm"></span>".</p> </div>
</td>
</tr>
</tbody>
<% } else { %> <% } else { %>
<tbody class="bg-white dark:bg-gray-800"> <div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
<tr> <p>Keine Bewerbungen gefunden.</p>
<td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400"> <p class="mt-2 text-sm">Klicken Sie auf "Bewerbung hinzufügen", um Ihre erste Bewerbung einzutragen.</p>
<p>Keine Bewerbungen gefunden.</p> </div>
<p class="mt-2 text-sm">Klicken Sie auf "Bewerbung hinzufügen", um Ihre erste Bewerbung einzutragen.</p>
</td>
</tr>
</tbody>
<% } %> <% } %>
</table> </div>
</div> </div>
<script> <script>
// Live company search — filters the applications table by firma/stelle.
(function () { (function () {
function qsa(sel, ctx) {
return Array.prototype.slice.call((ctx || document).querySelectorAll(sel));
}
// Öffnet/schließt einen klappbaren Bereich und dreht sein Chevron mit.
function setOpen(collapsible, chevron, open) {
if (!collapsible) return;
collapsible.classList.toggle('open', open);
if (chevron) chevron.classList.toggle('rotate', open);
}
// Status-Gruppen (Gesendet, Eingangsbestätigung …) auf-/zuklappen.
qsa('.group-toggle').forEach(function (btn) {
btn.addEventListener('click', function () {
var body = btn.parentElement.querySelector('.group-body');
var chev = btn.querySelector('.chevron');
setOpen(body, chev, !body.classList.contains('open'));
});
});
// Detailbereich einer einzelnen Bewerbung auf-/zuklappen.
qsa('.app-row.has-details').forEach(function (row) {
var line = row.querySelector('.app-line');
line.addEventListener('click', function (e) {
if (e.target.closest('.app-actions')) return; // Bearbeiten/Löschen nicht abfangen
var details = row.querySelector('.app-details');
var chev = line.querySelector('.chevron');
setOpen(details, chev, !details.classList.contains('open'));
});
});
// ---- Live-Firmensuche: filtert die Bewerbungsliste nach Firma/Stelle ----
var input = document.getElementById('firmaSearch'); var input = document.getElementById('firmaSearch');
if (!input) return; if (!input) return;
var clear = document.getElementById('firmaSearchClear'); var clear = document.getElementById('firmaSearchClear');
var appRows = Array.prototype.slice.call(document.querySelectorAll('tbody.app-row')); var sections = qsa('.status-group');
var groupHeaders = Array.prototype.slice.call(document.querySelectorAll('tbody.status-group'));
var noResults = document.getElementById('searchNoResults'); var noResults = document.getElementById('searchNoResults');
var noResultsTerm = document.getElementById('searchNoResultsTerm'); var noResultsTerm = document.getElementById('searchNoResultsTerm');
function filter() { function filter() {
var q = input.value.trim().toLowerCase(); var q = input.value.trim().toLowerCase();
if (clear) clear.classList.toggle('hidden', !q); if (clear) clear.classList.toggle('hidden', !q);
var anyVisible = false;
appRows.forEach(function (tb) { sections.forEach(function (sec) {
var match = !q || (tb.dataset.search || '').indexOf(q) !== -1; var rows = qsa('.app-row', sec);
tb.style.display = match ? '' : 'none'; var visible = 0;
}); rows.forEach(function (row) {
var match = !q || (row.dataset.search || '').indexOf(q) !== -1;
row.style.display = match ? '' : 'none';
if (match) visible++;
});
if (visible) anyVisible = true;
// Refresh each status group: hide headers with no visible rows, update counts. var countEl = sec.querySelector('[data-group-count]');
groupHeaders.forEach(function (gh) {
var visible = 0, node = gh.nextElementSibling;
while (node && !node.classList.contains('status-group')) {
if (node.classList.contains('app-row') && node.style.display !== 'none') visible++;
node = node.nextElementSibling;
}
gh.style.display = (visible === 0 && q) ? 'none' : '';
var countEl = gh.querySelector('[data-group-count]');
if (countEl) countEl.textContent = visible; if (countEl) countEl.textContent = visible;
sec.style.display = (visible === 0 && q) ? 'none' : '';
// Während der Suche passende Gruppen aufklappen, nach dem Löschen alles wieder zu.
var body = sec.querySelector('.group-body');
var chev = sec.querySelector('.group-toggle .chevron');
setOpen(body, chev, !!q && visible > 0);
}); });
var anyVisible = appRows.some(function (tb) { return tb.style.display !== 'none'; });
if (noResults) { if (noResults) {
noResults.style.display = (!anyVisible && q) ? '' : 'none'; noResults.style.display = (!anyVisible && q) ? '' : 'none';
if (noResultsTerm) noResultsTerm.textContent = input.value.trim(); if (noResultsTerm) noResultsTerm.textContent = input.value.trim();