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>
This commit is contained in:
2026-07-13 11:41:32 +02:00
co-authored by Claude
parent d1116d522d
commit e97d1b3687
6 changed files with 147 additions and 165 deletions
+23 -7
View File
@@ -195,6 +195,11 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
const bewerber = [
settings && settings.name ? `Name: ${settings.name}` : null,
settings && settings.adresse ? `Adresse: ${settings.adresse}` : null,
settings && settings.email ? `E-Mail: ${settings.email}` : null,
settings && settings.telefon ? `Telefon: ${settings.telefon}` : null,
settings && settings.ort ? `Wohnort: ${settings.ort}` : null,
settings && settings.webseite ? `Webseite: ${settings.webseite}` : null,
settings && settings.geburtsdatum ? `Geburtsdatum: ${settings.geburtsdatum}` : null,
].filter(Boolean).join('\n') || 'Keine Angaben';
const basisText = basisDokumente
@@ -327,10 +332,12 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
`"Facharzt", "Bachelor/Master", "Dipl.-..." u. Ä.). Im Zweifel eine neutrale, nicht geschützte ` +
`Tätigkeitsbezeichnung wählen (z. B. "Softwareentwickler", "IT-Fachkraft", "Anwendungsentwickler") ` +
`statt eines geschützten Titels.\n` +
`- kontakt: E-Mail, Telefon, Ort, Webseite, Geburtsdatum NUR übernehmen, wenn in den ` +
`Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden. Geburtsdatum als ` +
`DD.MM.YYYY (z. B. "03.04.1990"). Keine anderen persönlichen Angaben (Führerschein, ` +
`Familienstand, Nationalität o. Ä.) - diese Felder bleiben leer.\n` +
`- kontakt: E-Mail, Telefon, Ort (Wohnort), Webseite und Geburtsdatum werden DIR oben ` +
`unter "Bewerber" als feste Angaben geliefert. Übernimm sie WORTWÖRTLICH (Geburtsdatum ` +
`als DD.MM.YYYY, z. B. "03.04.1990"). Fehlt eine Angabe dort, übernimm sie nur, wenn sie ` +
`in den Basis-Unterlagen steht, sonst leerer String. NIEMALS erfinden. Keine anderen ` +
`persönlichen Angaben (Führerschein, Familienstand, Nationalität o. Ä.) - diese Felder ` +
`bleiben leer.\n` +
`- anschreiben.empfaenger: Empfängerblock. firma = Unternehmen; adresse = Straße + Hausnr. ` +
`(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` +
`ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` +
@@ -1834,11 +1841,20 @@ function renderSingleColumn(compose, t) {
}
function buildHeader(settings, kontakt, headline) {
const s = settings || {};
// Persönliche Kontaktfelder kommen verbindlich aus den Settings (unter "Vorlagen"
// gepflegt) sie gewinnen gegenüber dem vom Modell extrahierten Wert. Ein leeres
// Settings-Feld fällt auf den KI-Wert zurück, damit nichts verloren geht.
return {
name: (settings && settings.name) || '',
adresse: (settings && settings.adresse) || '',
name: s.name || '',
adresse: s.adresse || '',
headline,
...kontakt,
email: s.email || (kontakt && kontakt.email) || '',
telefon: s.telefon || (kontakt && kontakt.telefon) || '',
ort: s.ort || (kontakt && kontakt.ort) || '',
webseite: s.webseite || (kontakt && kontakt.webseite) || '',
geburtsdatum: s.geburtsdatum || (kontakt && kontakt.geburtsdatum) || '',
fuehrerschein: kontakt && kontakt.fuehrerschein,
};
}
-61
View File
@@ -9,29 +9,24 @@ const sunIcon = document.getElementById('sunIcon');
const moonIcon = document.getElementById('moonIcon');
// Modal Elements
const settingsModal = document.getElementById('settingsModal');
const applicationModal = document.getElementById('applicationModal');
const deleteModal = document.getElementById('deleteModal');
const pdfExportModal = document.getElementById('pdfExportModal');
// Form Elements
const settingsForm = document.getElementById('settingsForm');
const applicationForm = document.getElementById('applicationForm');
const pdfExportForm = document.getElementById('pdfExportForm');
const filterForm = document.getElementById('filterForm');
// Button Elements
const settingsBtn = document.getElementById('settingsBtn');
const addApplicationBtn = document.getElementById('addApplicationBtn');
const exportPdfBtn = document.getElementById('exportPdfBtn');
// Close Modal Buttons
const closeSettingsModal = document.getElementById('closeSettingsModal');
const closeApplicationModal = document.getElementById('closeApplicationModal');
const closeDeleteModal = document.getElementById('closeDeleteModal');
const closePdfModal = document.getElementById('closePdfModal');
const cancelSettings = document.getElementById('cancelSettings');
const cancelApplication = document.getElementById('cancelApplication');
const cancelDelete = document.getElementById('cancelDelete');
const cancelPdfExport = document.getElementById('cancelPdfExport');
@@ -96,51 +91,6 @@ function resetApplicationForm() {
currentApplicationId = null;
}
// ============================================
// Settings Management
// ============================================
function loadSettings() {
fetch('/api/settings')
.then(response => response.json())
.then(settings => {
if (settings) {
document.getElementById('userName').value = settings.name || '';
document.getElementById('userAddress').value = settings.adresse || '';
document.getElementById('customerNumber').value = settings.kundennummer || '';
}
})
.catch(error => console.error('Error loading settings:', error));
}
function saveSettings(event) {
event.preventDefault();
const formData = new FormData(settingsForm);
const settings = {
name: formData.get('name'),
adresse: formData.get('adresse'),
kundennummer: formData.get('kundennummer')
};
fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
})
.then(response => response.json())
.then(data => {
if (data.success) {
hideModal(settingsModal);
// Refresh page to update settings in header
location.reload();
}
})
.catch(error => console.error('Error saving settings:', error));
}
// ============================================
// Application Management (CRUD)
// ============================================
@@ -723,15 +673,6 @@ function loadPdfLibraries() {
// Dark Mode Toggle
darkModeToggle.addEventListener('click', toggleDarkMode);
// Settings Modal
settingsBtn.addEventListener('click', () => {
loadSettings();
showModal(settingsModal);
});
closeSettingsModal.addEventListener('click', () => hideModal(settingsModal));
cancelSettings.addEventListener('click', () => hideModal(settingsModal));
settingsForm.addEventListener('submit', saveSettings);
// Application Modal
addApplicationBtn.addEventListener('click', openAddApplicationModal);
closeApplicationModal.addEventListener('click', () => {
@@ -779,7 +720,6 @@ document.addEventListener('click', (e) => {
// Close modals when clicking outside
document.addEventListener('click', (e) => {
if (e.target === settingsModal) hideModal(settingsModal);
if (e.target === applicationModal) {
hideModal(applicationModal);
resetApplicationForm();
@@ -791,7 +731,6 @@ document.addEventListener('click', (e) => {
// Close modals with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!settingsModal.classList.contains('hidden')) hideModal(settingsModal);
if (!applicationModal.classList.contains('hidden')) {
hideModal(applicationModal);
resetApplicationForm();
+48 -10
View File
@@ -1009,19 +1009,32 @@ function initializeDatabase() {
id INTEGER PRIMARY KEY CHECK (id = 1),
name TEXT,
adresse TEXT,
kundennummer TEXT
kundennummer TEXT,
email TEXT,
telefon TEXT,
ort TEXT,
webseite TEXT,
geburtsdatum TEXT
)
`, (err) => {
if (err) return reject(err);
// Migration: persönliche Kontaktfelder nachträglich anlegen, falls die
// Tabelle aus einer älteren Version stammt (CREATE TABLE IF NOT EXISTS
// ergänzt fehlende Spalten nicht). Fehler "duplicate column" ignorieren.
const kontaktSpalten = ['email', 'telefon', 'ort', 'webseite', 'geburtsdatum'];
kontaktSpalten.forEach((spalte) => {
db.run(`ALTER TABLE settings ADD COLUMN ${spalte} TEXT`, () => {});
});
// Insert default settings if not exists
db.get('SELECT COUNT(*) as count FROM settings WHERE id = 1', (err, result) => {
if (err) return reject(err);
if (result && result.count === 0) {
db.run(
'INSERT INTO settings (id, name, adresse, kundennummer) VALUES (1, ?, ?, ?)',
['Max Mustermann', 'Musterstraße 1, 12345 Musterstadt', ''],
'INSERT INTO settings (id, name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)',
['Max Mustermann', 'Musterstraße 1, 12345 Musterstadt', '', '', '', '', '', '', ''],
(err) => {
if (err) return reject(err);
resolve();
@@ -1074,7 +1087,6 @@ initializeDatabase().then(() => {
const applications = await dbAll(query, params);
await attachVerlauf(applications);
applications.forEach((a) => { a.labelsArr = parseLabels(a.labels); });
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
// Upcoming calendar appointments for the dashboard widget.
const kommendeTermine = await upcomingTermine(6);
@@ -1118,7 +1130,6 @@ initializeDatabase().then(() => {
res.render('index', {
applications,
settings,
statistics: {
total: totalCount ? totalCount.count : 0,
byArt,
@@ -1170,13 +1181,17 @@ initializeDatabase().then(() => {
// Save settings
app.post('/api/settings', async (req, res) => {
try {
const { name, adresse, kundennummer } = req.body;
const { name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum } = req.body;
await dbRun(
'UPDATE settings SET name = ?, adresse = ?, kundennummer = ? WHERE id = 1',
[sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer)]
'UPDATE settings SET name = ?, adresse = ?, kundennummer = ?, email = ?, telefon = ?, ort = ?, webseite = ?, geburtsdatum = ? WHERE id = 1',
[
sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer),
sanitizeInput(email), sanitizeInput(telefon), sanitizeInput(ort),
sanitizeInput(webseite), sanitizeInput(geburtsdatum)
]
);
res.json({ success: true });
} catch (error) {
console.error('Error saving settings:', error);
@@ -1953,6 +1968,7 @@ initializeDatabase().then(() => {
res.render('vorlagen', {
basisDokumente,
basisAnhaenge,
settings: await dbGet('SELECT * FROM settings WHERE id = 1'),
prompts: promptStore.list(await loadPrompts()),
designFelder: designStore.list(design),
designAngepasst: designStore.isAngepasst(design),
@@ -1969,6 +1985,28 @@ initializeDatabase().then(() => {
}
});
// ----- Persönliche Angaben (under Vorlagen, formerly the index-page modal) -----
// Registered before /vorlagen/:id so the generic base-document handler doesn't
// swallow this path. Stores name/address/contact data that flows into every
// generated document (replacing the previous KI extraction from basis texts).
app.post('/vorlagen/persoenlich', async (req, res) => {
try {
const { name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum } = req.body;
await dbRun(
'UPDATE settings SET name = ?, adresse = ?, kundennummer = ?, email = ?, telefon = ?, ort = ?, webseite = ?, geburtsdatum = ? WHERE id = 1',
[
sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer),
sanitizeInput(email), sanitizeInput(telefon), sanitizeInput(ort),
sanitizeInput(webseite), sanitizeInput(geburtsdatum)
]
);
res.redirect('/vorlagen#persoenlich');
} catch (error) {
console.error('Error saving personal data:', error);
res.status(500).send('Serverfehler');
}
});
// ----- Editable KI prompts -----
// Registered before /vorlagen/:id so the generic base-document handlers don't
// swallow these paths. The prompt text is stored raw (no HTML escaping): it is
-72
View File
@@ -474,78 +474,6 @@
<%- include('partials/footer') %>
<!-- Settings Modal -->
<div id="settingsModal" 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">Benutzereinstellungen</h2>
<button id="closeSettingsModal" 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="settingsForm" class="space-y-4">
<div>
<label for="userName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Vollständiger Name *
</label>
<input
type="text"
id="userName"
name="name"
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="Max Mustermann"
value="<%= settings.name || '' %>">
</div>
<div>
<label for="userAddress" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Adresse *
</label>
<textarea
id="userAddress"
name="adresse"
required
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"
placeholder="Musterstraße 1, 12345 Musterstadt"><%= settings.adresse || '' %></textarea>
</div>
<div>
<label for="customerNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Jobcenter Kundennummer
</label>
<input
type="text"
id="customerNumber"
name="kundennummer"
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="z.B. 123456789"
value="<%= settings.kundennummer || '' %>">
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
id="cancelSettings"
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>
<!-- 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">
-14
View File
@@ -66,20 +66,6 @@
</svg>
</button>
<!-- Settings button -->
<% if (typeof hideSettings === 'undefined' || !hideSettings) { %>
<button
id="settingsBtn"
class="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-colors text-white"
aria-label="Einstellungen"
>
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</button>
<% } %>
<!-- Notification bell: unread received e-mails (incl. auto-assigned replies) -->
<div class="relative" id="notifWrap">
<button id="notifBtn" type="button"
+76 -1
View File
@@ -40,6 +40,81 @@
</div>
<% } %>
<!-- Persönliche Angaben (formerly the index-page settings modal) -->
<h2 id="persoenlich" class="text-xl font-bold text-gray-800 dark:text-white mt-10 mb-2 scroll-mt-4">Persönliche Angaben</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
Deine Kontaktdaten fließen in jede Bewerbung ein oben in Lebenslauf und Anschreiben
sowie in die Kontaktleiste. Die KI übernimmt sie unverändert und erfindet nichts mehr;
sie müssen dafür nicht länger in den Basis-Unterlagen stehen.
</p>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<form action="/vorlagen/persoenlich" method="POST" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Vollständiger Name *
</label>
<input type="text" id="name" name="name" 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="Max Mustermann" value="<%= settings.name || '' %>">
</div>
<div>
<label for="adresse" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Adresse
</label>
<textarea id="adresse" name="adresse" 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"
placeholder="Musterstraße 1, 12345 Musterstadt"><%= settings.adresse || '' %></textarea>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="email" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">E-Mail</label>
<input type="email" id="email" name="email"
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="name@example.com" value="<%= settings.email || '' %>">
</div>
<div>
<label for="telefon" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Telefon</label>
<input type="text" id="telefon" name="telefon"
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="z. B. 02043 123456" value="<%= settings.telefon || '' %>">
</div>
<div>
<label for="ort" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Wohnort</label>
<input type="text" id="ort" name="ort"
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="z. B. Gladbeck" value="<%= settings.ort || '' %>">
</div>
<div>
<label for="webseite" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Webseite</label>
<input type="text" id="webseite" name="webseite"
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="z. B. hackner.dev" value="<%= settings.webseite || '' %>">
</div>
<div>
<label for="geburtsdatum" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Geburtsdatum</label>
<input type="text" id="geburtsdatum" name="geburtsdatum"
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="TT.MM.JJJJ" value="<%= settings.geburtsdatum || '' %>">
</div>
<div>
<label for="kundennummer" class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Jobcenter-Kundennummer</label>
<input type="text" id="kundennummer" name="kundennummer"
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="z. B. 123456789" value="<%= settings.kundennummer || '' %>">
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors">
Speichern
</button>
</div>
</form>
</section>
<!-- Existing base documents -->
<section class="space-y-4 mb-8">
<% if (basisDokumente.length === 0) { %>
@@ -257,7 +332,7 @@
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
Aussehen von Anschreiben und Lebenslauf. <strong>Das Anschreiben übernimmt immer das Layout des
Lebenslaufs</strong>, damit beide Dokumente als ein Set ankommen. Der Lebenslauf passt in jedem Layout
garantiert auf eine Seite. Die Vorschau zeigt Musterinhalte mit deinem Namen, Foto und deiner
garantiert auf eine Seite. Die Vorschau zeigt Musterinhalte mit deinen Kontaktdaten, Foto und deiner
Unterschrift, ohne dass die KI läuft.
</p>