Files
jobbi-bewerbung/views/index.ejs
T
thomasandClaude e97d1b3687 Persoenliche Angaben unter Vorlagen statt hardcoded/Settings-Modal
- settings-Tabelle um email/telefon/ort/webseite/geburtsdatum erweitert
  (CREATE + ALTER-Migration + Default-INSERT)
- Vorlagen-Seite: neuer Bereich "Persoenliche Angaben" (8 Felder),
  POST /vorlagen/persoenlich; Settings-Modal + Zahnrad-Button entfernt
- Generierung: Kontakt kommt verbindlich aus den Settings (buildHeader
  laesst gespeicherte Werte ueber die KI-Extraktion gewinnen), bewerber-
  Fakten + Prompt angepasst; Design-Vorschau zeigt echte Kontaktdaten
- main.js: Settings-Modal-Handling entfernt; /api/settings bleibt bestehen

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 11:41:32 +02:00

730 lines
47 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<%- include('partials/head') %>
</head>
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
<%- include('partials/header') %>
<main class="flex-1 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 -->
<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>
<form id="filterForm" class="flex flex-wrap gap-4 items-end">
<div class="flex-1 min-w-32">
<label for="filterMonth" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Monat
</label>
<select
id="filterMonth"
name="month"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Alle Monate --</option>
<% availableMonths.forEach(m => { %>
<option value="<%= m.month %>"><%= m.month %> - <%= m.year %></option>
<% }); %>
</select>
</div>
<div class="flex-1 min-w-32">
<label for="filterYear" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Jahr
</label>
<select
id="filterYear"
name="year"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Alle Jahre --</option>
<% const years = [...new Set(availableMonths.map(m => m.year))].sort().reverse(); %>
<% years.forEach(y => { %>
<option value="<%= y %>"><%= y %></option>
<% }); %>
</select>
</div>
<div class="flex space-x-2">
<button
type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
Filtern
</button>
<a
href="/"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Zurücksetzen
</a>
</div>
<div class="ml-auto">
<button
type="button"
id="exportPdfBtn"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors flex items-center space-x-2">
<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="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>PDF Export</span>
</button>
</div>
<div>
<button
type="button"
id="addApplicationBtn"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors flex items-center space-x-2">
<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="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
<span>Bewerbung hinzufügen</span>
</button>
</div>
</form>
</div>
<!-- Upcoming appointments (from the application calendar) -->
<% if (typeof kommendeTermine !== 'undefined' && kommendeTermine && kommendeTermine.length) { %>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<div class="flex items-center gap-2 mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Anstehende Termine</h2>
</div>
<ul class="space-y-2">
<% kommendeTermine.forEach(function(t){
var when = t.ganztags
? new Date(t.start).toLocaleDateString('de-DE', { timeZone: caldavTz, weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' })
: new Date(t.start).toLocaleString('de-DE', { timeZone: caldavTz, weekday: 'short', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
%>
<li class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 px-3 py-2">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= t.typ === 'vorstellungsgespraech' ? 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200' : 'bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-200' %>"><%= t.typ === 'vorstellungsgespraech' ? 'Gespräch' : 'Termin' %></span>
<span class="text-sm font-medium text-gray-800 dark:text-gray-100 break-words"><%= t.titel %></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300 mt-0.5">
<%= when %> Uhr<% if (t.bewerbung_firma) { %> · <span class="text-gray-500 dark:text-gray-400"><%= t.bewerbung_firma %></span><% } %>
</p>
</div>
<% if (t.bewerbung_id) { %>
<a href="/bewerbung/<%= t.bewerbung_id %>#termine" class="shrink-0 inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline">Öffnen
<svg class="w-3.5 h-3.5" 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>
</a>
<% } %>
</li>
<% }); %>
</ul>
</div>
<% } %>
<!-- Statistics Section -->
<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-6">Statistik</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Total Applications -->
<div class="bg-blue-50 dark:bg-gray-700 rounded-lg p-4 text-center">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"><%= statistics.total %></div>
<div class="text-gray-600 dark:text-gray-400 mt-2">Gesamtbewerbungen</div>
</div>
<!-- By Application Type -->
<div class="bg-green-50 dark:bg-gray-700 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Nach Bewerbungsart</h3>
<div class="space-y-2">
<% if (statistics.byArt.length > 0) { %>
<% statistics.byArt.forEach(item => { %>
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400"><%= item.art %></span>
<span class="font-medium text-gray-800 dark:text-white"><%= item.count %></span>
</div>
<% }); %>
<% } else { %>
<p class="text-sm text-gray-500 dark:text-gray-400">Keine Daten verfügbar</p>
<% } %>
</div>
</div>
<!-- By Status -->
<div class="bg-purple-50 dark:bg-gray-700 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Nach Status</h3>
<div class="space-y-2">
<% if (statistics.byStatus.length > 0) { %>
<% statistics.byStatus.forEach(item => { %>
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400"><%= item.status %></span>
<span class="font-medium text-gray-800 dark:text-white"><%= item.count %></span>
</div>
<% }); %>
<% } else { %>
<p class="text-sm text-gray-500 dark:text-gray-400">Keine Daten verfügbar</p>
<% } %>
</div>
</div>
</div>
</div>
<!-- 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="min-w-[760px]">
<!-- Column labels -->
<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">
<span></span>
<span>Datum</span>
<span>Firma</span>
<span>Stelle</span>
<span>Art</span>
<span>Status</span>
<span>Aktionen</span>
</div>
<% if (applications.length > 0) { %>
<%
// Status-Reihenfolge synchron zu public/js/main.js halten
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Einstellung', 'Absage', 'Keine Rückmeldung'];
const statusDot = {
'Entwurf': 'bg-purple-500',
'Gesendet': 'bg-indigo-500',
'Eingangsbestätigung': 'bg-blue-500',
'In Bearbeitung': 'bg-teal-500',
'Interessiert': 'bg-pink-500',
'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500',
'Einstellung': 'bg-green-500',
'Absage': 'bg-red-500',
'Keine Rückmeldung': 'bg-gray-400',
'Ohne Status': 'bg-gray-300'
};
const groups = {};
applications.forEach(a => {
const key = (a.status && a.status.trim()) ? a.status.trim() : 'Ohne Status';
(groups[key] = groups[key] || []).push(a);
});
const orderedKeys = [
...statusOrder.filter(s => groups[s]),
...Object.keys(groups).filter(k => !statusOrder.includes(k))
];
%>
<div id="appList">
<% orderedKeys.forEach(statusKey => { %>
<section class="status-group border-t border-gray-200 dark:border-gray-700">
<!-- Klappbarer Status-Kopf (Gesendet, Eingangsbestätigung, …) -->
<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">
<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">
<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 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">
<span class="block truncate text-gray-900 dark:text-white" title="<%= app.stelle %>"><%= app.stelle %></span>
<% if (app.labelsArr && app.labelsArr.length) { %>
<span class="flex flex-wrap gap-1 mt-1"><%- include('partials/labelChips', { labels: app.labelsArr }) %></span>
<% } %>
</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 === 'In Bearbeitung' ? 'bg-teal-100 text-teal-800 dark:bg-teal-800 dark:text-teal-100' :
app.status === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' :
app.status === 'Warten auf Rückmeldung' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
app.status === 'Warten auf meine Antwort' ? 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-800 dark:text-fuchsia-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>
</div>
</section>
<% }); %>
</div>
<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">
<p>Keine Firma gefunden für „<span id="searchNoResultsTerm"></span>".</p>
</div>
<% } else { %>
<div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
<p>Keine Bewerbungen gefunden.</p>
<p class="mt-2 text-sm">Klicken Sie auf "Bewerbung hinzufügen", um Ihre erste Bewerbung einzutragen.</p>
</div>
<% } %>
</div>
</div>
<script>
(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');
if (!input) return;
var clear = document.getElementById('firmaSearchClear');
var sections = qsa('.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);
var anyVisible = false;
sections.forEach(function (sec) {
var rows = qsa('.app-row', sec);
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;
var countEl = sec.querySelector('[data-group-count]');
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);
});
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>
<%- include('partials/footer') %>
<!-- Add/Edit Application Modal -->
<div id="applicationModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg p-6">
<div class="flex justify-between items-center mb-6">
<h2 id="modalTitle" class="text-xl font-bold text-gray-800 dark:text-white">Bewerbung hinzufügen</h2>
<button id="closeApplicationModal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-6 h-6" 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>
<form id="applicationForm" class="space-y-4">
<input type="hidden" id="applicationId" name="id">
<div>
<label for="applicationDatum" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Datum *
</label>
<input
type="date"
id="applicationDatum"
name="datum"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
<div>
<label for="applicationFirma" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Firma *
</label>
<input
type="text"
id="applicationFirma"
name="firma"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
placeholder="Firmenname">
</div>
<div>
<label for="applicationStelle" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Stelle *
</label>
<input
type="text"
id="applicationStelle"
name="stelle"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
placeholder="Stellenbezeichnung">
</div>
<div>
<label for="applicationArt" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Art der Bewerbung
</label>
<select
id="applicationArt"
name="art"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Bitte wählen --</option>
<% artOptions.forEach(option => { %>
<option value="<%= option %>"><%= option %></option>
<% }); %>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Labels
</label>
<div id="applicationLabels">
<%- include('partials/labelPicker', { labelOptions: labelOptions, selected: [] }) %>
</div>
</div>
<div>
<label for="applicationStatus" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Status (Anfangsstatus)
</label>
<select
id="applicationStatus"
name="status"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Bitte wählen --</option>
<% statusOptions.forEach(option => { %>
<option value="<%= option %>"><%= option %></option>
<% }); %>
</select>
</div>
<div>
<label for="applicationKommentar" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Kommentar zur Statusänderung
</label>
<textarea
id="applicationKommentar"
name="kommentar"
rows="2"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Optional z.B. „per E-Mail an Frau Müller“"></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Weitere Statusänderungen dokumentierst du später auf der Bearbeiten-Seite.</p>
</div>
<div>
<label for="applicationNotizen" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Notizen
</label>
<textarea
id="applicationNotizen"
name="notizen"
rows="4"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Allgemeine Notizen zur Bewerbung..."></textarea>
</div>
<div>
<label for="applicationInterneNotizen" class="block text-sm font-medium text-amber-700 dark:text-amber-400 mb-2 flex items-center gap-1.5">
<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="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>
Interne Notizen (nicht im PDF)
</label>
<textarea
id="applicationInterneNotizen"
name="interne_notizen"
rows="3"
class="w-full px-3 py-2 border border-amber-300 dark:border-amber-700 rounded-md bg-amber-50 dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Nur für dich erscheint nicht im Export..."></textarea>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
id="cancelApplication"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Abbrechen
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Bewerbung löschen</h2>
<button id="closeDeleteModal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-6 h-6" 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>
<p class="text-gray-600 dark:text-gray-400 mb-6">
Sind Sie sicher, dass Sie diese Bewerbung löschen möchten? Dieser Vorgang kann nicht rückgängig gemacht werden.
</p>
<div class="flex justify-end space-x-3">
<button
type="button"
id="cancelDelete"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Abbrechen
</button>
<button
type="button"
id="confirmDelete"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors">
Löschen
</button>
</div>
</div>
</div>
</div>
<!-- PDF Export Modal -->
<div id="pdfExportModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white">PDF Export</h2>
<button id="closePdfModal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-6 h-6" 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>
<form id="pdfExportForm" class="space-y-4">
<div>
<label for="pdfMonth" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Monat
</label>
<select
id="pdfMonth"
name="month"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Alle Monate --</option>
<% exportMonths.forEach(m => { %>
<option value="<%= m.year %>-<%= m.month %>"><%= m.month %> - <%= m.year %></option>
<% }); %>
</select>
</div>
<div>
<label for="pdfYear" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Jahr
</label>
<select
id="pdfYear"
name="year"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Alle Jahre --</option>
<% const pdfYears = [...new Set(exportMonths.map(m => m.year))].sort().reverse(); %>
<% pdfYears.forEach(y => { %>
<option value="<%= y %>"><%= y %></option>
<% }); %>
</select>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
id="cancelPdfExport"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Abbrechen
</button>
<button
type="submit"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors">
PDF generieren
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Load main JavaScript -->
<script src="/js/main.js"></script>
</body>
</html>