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:
+141
-88
@@ -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,42 +243,43 @@
|
|||||||
...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>
|
||||||
|
</svg>
|
||||||
|
<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 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>
|
<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>
|
</button>
|
||||||
</td>
|
<div class="group-body collapsible">
|
||||||
</tr>
|
<div class="collapsible-inner">
|
||||||
</tbody>
|
|
||||||
<% groups[statusKey].forEach(app => { %>
|
<% groups[statusKey].forEach(app => { %>
|
||||||
<%
|
<%
|
||||||
const hatNotizen = app.notizen && app.notizen.trim().length > 0;
|
const hatNotizen = app.notizen && app.notizen.trim().length > 0;
|
||||||
const hatInterne = app.interne_notizen && app.interne_notizen.trim().length > 0;
|
const hatInterne = app.interne_notizen && app.interne_notizen.trim().length > 0;
|
||||||
const hatVerlauf = app.verlauf && app.verlauf.length > 0;
|
const hatVerlauf = app.verlauf && app.verlauf.length > 0;
|
||||||
const hatDetails = hatNotizen || hatInterne || hatVerlauf;
|
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, '') %>">
|
<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, '') %>">
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<!-- Eine Zeile pro Bewerbung -->
|
||||||
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
<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' : '' %>">
|
||||||
<%= new Date(app.datum).toLocaleDateString('de-DE') %>
|
<span class="flex items-center justify-center">
|
||||||
</td>
|
<% if (hatDetails) { %>
|
||||||
<td class="px-6 pt-4 <%= padB %> align-top text-sm font-medium text-gray-900 dark:text-white break-words">
|
<svg class="chevron w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<%= app.firma %>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
</td>
|
</svg>
|
||||||
<td class="px-6 pt-4 <%= padB %> align-top text-sm text-gray-900 dark:text-white break-words">
|
<% } %>
|
||||||
<%= app.stelle %>
|
</span>
|
||||||
</td>
|
<span class="whitespace-nowrap text-gray-900 dark:text-white"><%= new Date(app.datum).toLocaleDateString('de-DE') %></span>
|
||||||
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
<span class="min-w-0 truncate font-medium text-gray-900 dark:text-white" title="<%= app.firma %>"><%= app.firma %></span>
|
||||||
<%= app.art || '-' %>
|
<span class="min-w-0 truncate text-gray-900 dark:text-white" title="<%= app.stelle %>"><%= app.stelle %></span>
|
||||||
</td>
|
<span class="min-w-0 truncate text-gray-500 dark:text-gray-400"><%= app.art || '-' %></span>
|
||||||
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm">
|
<span class="min-w-0">
|
||||||
<span class="px-2 py-1 rounded-full text-xs font-medium
|
<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 === '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 === '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 === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' :
|
||||||
@@ -266,27 +287,29 @@
|
|||||||
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
|
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
|
||||||
<%= app.status || '-' %>
|
<%= app.status || '-' %>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</span>
|
||||||
<td class="px-6 pt-4 <%= padB %> whitespace-nowrap text-sm align-top">
|
<span class="app-actions flex items-center gap-3">
|
||||||
<a href="/bewerbung/<%= app.id %>"
|
<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"
|
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
aria-label="Bearbeiten">
|
aria-label="Bearbeiten">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button type="button"
|
||||||
class="delete-btn align-middle text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
class="delete-btn text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||||
data-id="<%= app.id %>">
|
data-id="<%= app.id %>" aria-label="Löschen">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</span>
|
||||||
</tr>
|
</div>
|
||||||
<% if (hatDetails) { %>
|
<% if (hatDetails) { %>
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<!-- Klappbarer Detailbereich unter der Zeile -->
|
||||||
<td colspan="6" class="px-6 pb-5 pt-0 space-y-3">
|
<div class="app-details collapsible">
|
||||||
|
<div class="collapsible-inner">
|
||||||
|
<div class="px-6 pb-5 pt-1 space-y-3">
|
||||||
<% if (hatVerlauf) { %>
|
<% 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="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">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
@@ -330,65 +353,95 @@
|
|||||||
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.interne_notizen %></p>
|
<p class="whitespace-pre-wrap break-words text-base leading-relaxed text-gray-800 dark:text-gray-200"><%= app.interne_notizen %></p>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</tbody>
|
</div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
<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>
|
||||||
</td>
|
</div>
|
||||||
</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>
|
|
||||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<p>Keine Bewerbungen gefunden.</p>
|
<p>Keine Bewerbungen gefunden.</p>
|
||||||
<p class="mt-2 text-sm">Klicken Sie auf "Bewerbung hinzufügen", um Ihre erste Bewerbung einzutragen.</p>
|
<p class="mt-2 text-sm">Klicken Sie auf "Bewerbung hinzufügen", um Ihre erste Bewerbung einzutragen.</p>
|
||||||
</td>
|
</div>
|
||||||
</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();
|
||||||
|
|||||||
Reference in New Issue
Block a user