Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 18:15:11 +02:00
commit c65c9f1751
19 changed files with 3633 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
<%- include('partials/header') %>
<div style="max-width:42rem;">
<div class="page-header" style="margin-bottom:1.5rem;">
<div>
<h1 class="page-title">Einstellungen</h1>
<p class="page-meta">Diese Daten erscheinen automatisch in jedem PDF-Export.</p>
</div>
</div>
<% if (gespeichert) { %>
<div class="alert alert-success">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Einstellungen wurden gespeichert.
</div>
<% } %>
<div class="settings-card">
<form method="POST" action="/einstellungen" class="settings-form">
<div class="form-group">
<label class="form-label" for="name">Vollständiger Name</label>
<input type="text" id="name" name="name" maxlength="500" class="form-input"
value="<%= settings.name || '' %>"
placeholder="Vorname Nachname">
<p class="form-hint">Erscheint als Absender und in der Unterschriftszeile des PDFs.</p>
</div>
<div class="form-group">
<label class="form-label" for="adresse">Adresse</label>
<textarea id="adresse" name="adresse" rows="3" maxlength="1000" class="form-textarea"
placeholder="Musterstraße 1&#10;12345 Musterstadt"><%= settings.adresse || '' %></textarea>
<p class="form-hint">Mehrzeilig erscheint im Briefkopf des PDFs.</p>
</div>
<div class="form-group">
<label class="form-label" for="kundennummer">Jobcenter-Kundennummer</label>
<input type="text" id="kundennummer" name="kundennummer" maxlength="100" class="form-input"
value="<%= settings.kundennummer || '' %>"
placeholder="z.B. BG-12345678">
<p class="form-hint">Ihre Kundennummer beim Jobcenter / der Agentur für Arbeit.</p>
</div>
<div style="display:flex;justify-content:flex-end;padding-top:.5rem;">
<button type="submit" class="btn btn-primary">Einstellungen speichern</button>
</div>
</form>
</div>
<div class="info-box" style="margin-top:1rem;">
<strong>PDF-Hinweis</strong>
Das exportierte PDF enthält Ihren Briefkopf mit den obigen Daten, einen Zusammenfassungssatz,
eine vollständige Bewerbungstabelle sowie eine Bestätigungs- und Unterschriftszeile.
Starten Sie den Export über „PDF exportieren" auf der Übersichtsseite.
</div>
</div>
<%- include('partials/footer') %>
+298
View File
@@ -0,0 +1,298 @@
<%- include('partials/header') %>
<%# ── Embed server data for client-side use ──────────────────────────────── %>
<script type="application/json" id="bewerbungenData"><%- bewerbungenJson %></script>
<script>
const CURRENT_MONAT = <%= monat %>;
const CURRENT_JAHR = <%= jahr %>;
const MONATE_DE = <%- JSON.stringify(monate) %>;
</script>
<%# ── Page header ────────────────────────────────────────────────────────── %>
<div class="page-header">
<div>
<h1 class="page-title">Bewerbungsübersicht</h1>
<div class="page-meta">
<span><%= monatName %> <%= jahr %></span>
<% if (settings && settings.kundennummer) { %>
<span class="sep">&middot;</span>
<span class="tag">KdNr. <%= settings.kundennummer %></span>
<% } %>
</div>
</div>
<%# Filter + PDF %>
<form method="GET" action="/" class="filter-bar">
<select name="monat" class="form-select btn-sm" style="width:auto;">
<% monate.forEach((m, i) => { %>
<option value="<%= i + 1 %>" <%= (i + 1 === monat) ? 'selected' : '' %>><%= m %></option>
<% }) %>
</select>
<select name="jahr" class="form-select btn-sm" style="width:auto;">
<% jahre.forEach(j => { %>
<option value="<%= j %>" <%= (j === jahr) ? 'selected' : '' %>><%= j %></option>
<% }) %>
</select>
<button type="submit" class="btn btn-secondary btn-sm">Filtern</button>
<button type="button" class="btn btn-primary btn-sm" onclick="generatePDF(<%= monat %>, <%= jahr %>)">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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"/>
</svg>
PDF exportieren
</button>
</form>
</div>
<%# ── Error banner ───────────────────────────────────────────────────────── %>
<% if (fehler === 'pflichtfelder') { %>
<div class="alert alert-error">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
Bitte alle Pflichtfelder ausfüllen: Datum, Firma und Stelle sind erforderlich.
</div>
<% } %>
<%# ── Stats grid ─────────────────────────────────────────────────────────── %>
<div class="stats-grid">
<div class="stat-card s-total">
<div class="stat-label">Gesamt</div>
<div class="stat-value" data-count="<%= stats.gesamt %>"><%= stats.gesamt %></div>
</div>
<div class="stat-card s-positiv">
<div class="stat-label">Positiv</div>
<div class="stat-value" data-count="<%= stats.positiv %>"><%= stats.positiv %></div>
</div>
<div class="stat-card s-absage">
<div class="stat-label">Absagen</div>
<div class="stat-value" data-count="<%= stats.absagen %>"><%= stats.absagen %></div>
</div>
<div class="stat-card s-pending">
<div class="stat-label">Ausstehend</div>
<div class="stat-value" data-count="<%= stats.ausstehend %>"><%= stats.ausstehend %></div>
</div>
</div>
<%# ── Action bar ─────────────────────────────────────────────────────────── %>
<div class="action-bar">
<span class="entry-count"><%= bewerbungen.length %> Eintr<%= bewerbungen.length === 1 ? 'ag' : 'äge' %></span>
<button class="btn btn-primary btn-sm" onclick="openAddModal()">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/>
</svg>
Neue Bewerbung
</button>
</div>
<%# ── Data table ─────────────────────────────────────────────────────────── %>
<div class="data-card">
<% if (bewerbungen.length === 0) { %>
<div class="empty-state">
<div class="empty-icon">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
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"/>
</svg>
</div>
<p class="empty-title">Keine Bewerbungen für <%= monatName %> <%= jahr %></p>
<p class="empty-sub">Klicken Sie auf „Neue Bewerbung", um Ihren ersten Eintrag hinzuzufügen.</p>
</div>
<% } else { %>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th>Datum</th>
<th>Firma</th>
<th>Stelle</th>
<th>Art</th>
<th>Status</th>
<th class="hidden md:table-cell">Notizen</th>
<th style="text-align:right; padding-right:1.25rem;">Aktionen</th>
</tr>
</thead>
<tbody>
<% bewerbungen.forEach(b => { %>
<tr>
<td class="td-date">
<%= b.datum ? b.datum.split('-').reverse().join('.') : '' %>
</td>
<td class="td-firma"><%= b.firma %></td>
<td class="td-stelle"><%= b.stelle %></td>
<td>
<% if (b.art) { %>
<span class="td-art"><%= b.art %></span>
<% } %>
</td>
<td>
<% if (b.status) { %>
<span class="status-chip <%= statusClass(b.status) %>"><%= b.status %></span>
<% } %>
</td>
<td class="td-notizen hidden md:table-cell"><%= b.notizen || '' %></td>
<td>
<div class="row-actions flex items-center justify-end gap-1" style="padding-right:0.25rem;">
<button class="row-btn btn-edit" title="Bearbeiten"
onclick="openEditModal(<%= b.id %>)">
<svg width="15" height="15" 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"/>
</svg>
</button>
<button class="row-btn btn-del" title="Löschen"
onclick="openDeleteModal(<%= b.id %>, '<%= b.firma.replace(/\\/g, '\\\\').replace(/'/g, "\\'") %>', '<%= b.stelle.replace(/\\/g, '\\\\').replace(/'/g, "\\'") %>')">
<svg width="15" height="15" 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"/>
</svg>
</button>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</div>
<%# ══════════════════════════════════════════════════════════════════════════ %>
<%# Modal: Neue / Bewerbung bearbeiten %>
<%# ══════════════════════════════════════════════════════════════════════════ %>
<div id="bewerbungModal" class="modal-backdrop" onclick="closeOnBackdrop(event,'bewerbungModal')">
<div class="modal-box" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-head">
<h2 id="modalTitle" class="modal-title">Neue Bewerbung</h2>
<button class="modal-close" onclick="closeModal('bewerbungModal')" aria-label="Schließen">
<svg width="16" height="16" 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"/>
</svg>
</button>
</div>
<form id="bewerbungForm" method="POST" action="/bewerbungen">
<input type="hidden" name="monat" value="<%= monat %>">
<input type="hidden" name="jahr" value="<%= jahr %>">
<div class="modal-body">
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="datum">Datum <span class="req">*</span></label>
<input type="date" id="datum" name="datum" required class="form-input">
</div>
<div class="form-group">
<label class="form-label" for="art">Art der Bewerbung</label>
<select id="art" name="art" class="form-select">
<option value="">— bitte wählen —</option>
<% artOptionen.forEach(a => { %>
<option value="<%= a %>"><%= a %></option>
<% }) %>
</select>
</div>
<div class="form-group span-2">
<label class="form-label" for="firma">Firma / Unternehmen <span class="req">*</span></label>
<input type="text" id="firma" name="firma" required maxlength="500" class="form-input"
placeholder="z.B. Muster GmbH">
</div>
<div class="form-group span-2">
<label class="form-label" for="stelle">Stelle / Position <span class="req">*</span></label>
<input type="text" id="stelle" name="stelle" required maxlength="500" class="form-input"
placeholder="z.B. Fachinformatiker / Softwareentwickler">
</div>
<div class="form-group span-2">
<label class="form-label" for="status">Status</label>
<select id="status" name="status" class="form-select">
<option value="">— bitte wählen —</option>
<% statusOptionen.forEach(s => { %>
<option value="<%= s %>"><%= s %></option>
<% }) %>
</select>
</div>
<div class="form-group span-2">
<label class="form-label" for="notizen">Notizen</label>
<textarea id="notizen" name="notizen" rows="3" maxlength="2000" class="form-textarea"
placeholder="Ansprechpartner, Referenznummer, Gehaltsvorstellung …"></textarea>
</div>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn btn-secondary" onclick="closeModal('bewerbungModal')">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
<%# ══════════════════════════════════════════════════════════════════════════ %>
<%# Modal: Löschen bestätigen %>
<%# ══════════════════════════════════════════════════════════════════════════ %>
<div id="deleteModal" class="modal-backdrop" onclick="closeOnBackdrop(event,'deleteModal')">
<div class="modal-box compact" role="dialog" aria-modal="true">
<div class="modal-head">
<h2 class="modal-title">Eintrag löschen</h2>
<button class="modal-close" onclick="closeModal('deleteModal')" aria-label="Schließen">
<svg width="16" height="16" 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"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="flex gap-3">
<div class="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full"
style="background:var(--red-dim);">
<svg width="18" height="18" fill="none" stroke="var(--red)" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div>
<p style="font-size:.9375rem;font-weight:600;color:var(--text);margin-bottom:.375rem;">
Sicher löschen?
</p>
<p id="deleteInfo" style="font-size:.875rem;color:var(--text-2);margin-bottom:.5rem;"></p>
<p style="font-size:.75rem;color:var(--text-muted);">
Diese Aktion kann nicht rückgängig gemacht werden.
</p>
</div>
</div>
</div>
<form id="deleteForm" method="POST">
<input type="hidden" name="monat" value="<%= monat %>">
<input type="hidden" name="jahr" value="<%= jahr %>">
<div class="modal-foot">
<button type="button" class="btn btn-secondary" onclick="closeModal('deleteModal')">Abbrechen</button>
<button type="submit" class="btn btn-danger">Endgültig löschen</button>
</div>
</form>
</div>
</div>
<%# Server-side helper: map status string → CSS class ─────────────────────── %>
<% function statusClass(s) {
const map = {
'Gesendet': 'st-gesendet',
'Eingangsbestätigung': 'st-eingang',
'Vorstellungsgespräch': 'st-vorstellung',
'Absage': 'st-absage',
'Einstellung': 'st-einstellung',
'Keine Rückmeldung': 'st-keine'
};
return map[s] || 'st-keine';
} %>
<%- include('partials/footer') %>
+9
View File
@@ -0,0 +1,9 @@
</div><%# /page-wrapper %>
<footer class="app-footer">
Bewerbungs-Tracker &mdash; Lokale Bewerbungsverwaltung
</footer>
<script src="/js/main.js"></script>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bewerbungs-Tracker</title>
<%# Synchronously apply saved dark mode preference before paint to prevent flash %>
<script>
(function () {
if (localStorage.getItem('darkMode') !== 'false') {
document.documentElement.classList.add('dark');
}
})();
</script>
<%# Tailwind CDN — used for layout utilities (flex, grid, overflow, responsive) %>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
corePlugins: { preflight: false }
};
</script>
<%# jsPDF + autoTable for client-side PDF generation %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="app-nav">
<a href="/" class="nav-brand">
<span class="brand-dot"></span>
Bewerbungs-Tracker
</a>
<div class="nav-links">
<a href="/" class="nav-link <%= (typeof currentPage !== 'undefined' && currentPage === 'uebersicht') ? 'active' : '' %>">Übersicht</a>
<a href="/einstellungen" class="nav-link <%= (typeof currentPage !== 'undefined' && currentPage === 'einstellungen') ? 'active' : '' %>">Einstellungen</a>
</div>
<button id="darkModeToggle" class="dark-toggle" title="Darkmodus umschalten" aria-label="Darkmodus umschalten"></button>
</nav>
<div class="page-wrapper">