Add centered live company search to the overview
A modern search bar at the top center of the index page filters the applications table live by firma/stelle. Status group headers hide when their group has no visible rows, per-group counts update with the filtered result, and an empty-state row shows when nothing matches. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+73
-3
@@ -7,6 +7,21 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="flex justify-center mb-8">
|
||||||
|
<div class="relative w-full max-w-xl">
|
||||||
|
<svg class="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-gray-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<input id="firmaSearch" type="text" placeholder="Firma suchen …" autocomplete="off"
|
||||||
|
class="w-full pl-12 pr-10 py-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition">
|
||||||
|
<button id="firmaSearchClear" type="button" aria-label="Suche zurücksetzen"
|
||||||
|
class="hidden absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filter Section -->
|
<!-- Filter Section -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
|
||||||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Filter</h2>
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Filter</h2>
|
||||||
@@ -167,13 +182,13 @@
|
|||||||
];
|
];
|
||||||
%>
|
%>
|
||||||
<% orderedKeys.forEach(statusKey => { %>
|
<% orderedKeys.forEach(statusKey => { %>
|
||||||
<tbody class="bg-gray-100 dark:bg-gray-900/40 border-b border-gray-200 dark:border-gray-700">
|
<tbody class="status-group bg-gray-100 dark:bg-gray-900/40 border-b border-gray-200 dark:border-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="px-6 py-3">
|
<td colspan="6" class="px-6 py-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="inline-block w-2.5 h-2.5 rounded-full <%= statusDot[statusKey] || 'bg-gray-300' %>"></span>
|
<span class="inline-block w-2.5 h-2.5 rounded-full <%= 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 class="text-sm font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-200"><%= statusKey %></span>
|
||||||
<span 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 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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -186,7 +201,7 @@
|
|||||||
const hatDetails = hatNotizen || hatInterne || hatVerlauf;
|
const hatDetails = hatNotizen || hatInterne || hatVerlauf;
|
||||||
const padB = hatDetails ? 'pb-2' : 'pb-4';
|
const padB = hatDetails ? 'pb-2' : 'pb-4';
|
||||||
%>
|
%>
|
||||||
<tbody class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
<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">
|
<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">
|
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
<%= new Date(app.datum).toLocaleDateString('de-DE') %>
|
<%= new Date(app.datum).toLocaleDateString('de-DE') %>
|
||||||
@@ -279,6 +294,13 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
|
<tbody id="searchNoResults" class="bg-white dark:bg-gray-800" style="display:none">
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<tbody class="bg-white dark:bg-gray-800">
|
<tbody class="bg-white dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -291,6 +313,54 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Live company search — filters the applications table by firma/stelle.
|
||||||
|
(function () {
|
||||||
|
var input = document.getElementById('firmaSearch');
|
||||||
|
if (!input) return;
|
||||||
|
var clear = document.getElementById('firmaSearchClear');
|
||||||
|
var appRows = Array.prototype.slice.call(document.querySelectorAll('tbody.app-row'));
|
||||||
|
var groupHeaders = Array.prototype.slice.call(document.querySelectorAll('tbody.status-group'));
|
||||||
|
var noResults = document.getElementById('searchNoResults');
|
||||||
|
var noResultsTerm = document.getElementById('searchNoResultsTerm');
|
||||||
|
|
||||||
|
function filter() {
|
||||||
|
var q = input.value.trim().toLowerCase();
|
||||||
|
if (clear) clear.classList.toggle('hidden', !q);
|
||||||
|
|
||||||
|
appRows.forEach(function (tb) {
|
||||||
|
var match = !q || (tb.dataset.search || '').indexOf(q) !== -1;
|
||||||
|
tb.style.display = match ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh each status group: hide headers with no visible rows, update counts.
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
var anyVisible = appRows.some(function (tb) { return tb.style.display !== 'none'; });
|
||||||
|
if (noResults) {
|
||||||
|
noResults.style.display = (!anyVisible && q) ? '' : 'none';
|
||||||
|
if (noResultsTerm) noResultsTerm.textContent = input.value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', filter);
|
||||||
|
if (clear) clear.addEventListener('click', function () {
|
||||||
|
input.value = '';
|
||||||
|
filter();
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
|||||||
Reference in New Issue
Block a user