Einrichtung: Setup-Assistent fuer neue Nutzer auf der Startseite
Nutzer ohne Bewerbungen sehen einen mehrstufigen Wizard, der Persoenliche
Angaben, Basis-Unterlagen und Ollama-KI per fetch speichert. Dashboard bleibt
im DOM (hidden), damit main.js und "Bewerbung manuell anlegen" funktionieren.
Neue Endpunkte /api/einrichtung/{persoenlich,basis,ollama,verstecken} mit
SSRF-Guard; Ollama gezielt via setForUser, ohne Mail/Kalender/Token zu leeren.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1707,6 +1707,30 @@ initializeDatabase().then(async () => {
|
|||||||
ORDER BY yearmonth DESC
|
ORDER BY yearmonth DESC
|
||||||
`, [U, U]);
|
`, [U, U]);
|
||||||
|
|
||||||
|
// Einrichtungsassistent: eingeblendet, wenn der Nutzer noch keine
|
||||||
|
// Bewerbung angelegt hat und den Assistenten nicht dauerhaft ausgeblendet
|
||||||
|
// hat. Der Wizard selbst führt dann durch die fehlenden Einstellungen
|
||||||
|
// (Persönliche Angaben, Basis-Unterlagen, Ollama-KI).
|
||||||
|
const keinDashboard = !totalCount || totalCount.count === 0;
|
||||||
|
const verstecktRow = keinDashboard
|
||||||
|
? await dbGet("SELECT value FROM app_state WHERE user_id = ? AND key = 'state:einrichtung_versteckt'", [U])
|
||||||
|
: null;
|
||||||
|
let setupWizard = { aktiv: false };
|
||||||
|
if (keinDashboard && !(verstecktRow && verstecktRow.value === '1')) {
|
||||||
|
const basisRows = await dbAll('SELECT id, typ, inhalt FROM basis_dokumente WHERE user_id = ? ORDER BY id ASC', [U]);
|
||||||
|
const anschreiben = basisRows.find((d) => d.typ === 'Anschreiben');
|
||||||
|
const lebenslauf = basisRows.find((d) => d.typ === 'Lebenslauf');
|
||||||
|
setupWizard = {
|
||||||
|
aktiv: true,
|
||||||
|
persoenlich: await loadSettings(),
|
||||||
|
anschreiben: anschreiben ? { id: anschreiben.id, inhalt: anschreiben.inhalt || '' } : null,
|
||||||
|
lebenslauf: lebenslauf ? { id: lebenslauf.id, inhalt: lebenslauf.inhalt || '' } : null,
|
||||||
|
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
|
||||||
|
ollamaModel: config.get('OLLAMA_MODEL'),
|
||||||
|
ollamaHost: config.get('OLLAMA_HOST'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
applications,
|
applications,
|
||||||
statistics: {
|
statistics: {
|
||||||
@@ -1722,7 +1746,8 @@ initializeDatabase().then(async () => {
|
|||||||
caldavTz: caldav.TZ,
|
caldavTz: caldav.TZ,
|
||||||
artOptions: ART_OPTIONS,
|
artOptions: ART_OPTIONS,
|
||||||
statusOptions: STATUS_OPTIONS,
|
statusOptions: STATUS_OPTIONS,
|
||||||
labelOptions: LABEL_OPTIONS
|
labelOptions: LABEL_OPTIONS,
|
||||||
|
setupWizard
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
@@ -1967,6 +1992,93 @@ initializeDatabase().then(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- Einrichtungsassistent (Onboarding auf der Startseite) -----
|
||||||
|
// JSON-Endpunkte, die der Wizard Schritt für Schritt per fetch anstößt — ohne
|
||||||
|
// Seitenwechsel, damit der Nutzer im Flow bleibt. Jeder speichert für den
|
||||||
|
// aktuellen Benutzer und antwortet mit { success } bzw. { error }.
|
||||||
|
app.post('/api/einrichtung/persoenlich', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum } = req.body;
|
||||||
|
await dbRun(
|
||||||
|
`INSERT INTO settings (user_id, name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(user_id) DO UPDATE SET
|
||||||
|
name = excluded.name, adresse = excluded.adresse, kundennummer = excluded.kundennummer,
|
||||||
|
email = excluded.email, telefon = excluded.telefon, ort = excluded.ort,
|
||||||
|
webseite = excluded.webseite, geburtsdatum = excluded.geburtsdatum`,
|
||||||
|
[uid(), sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer),
|
||||||
|
sanitizeInput(email), sanitizeInput(telefon), sanitizeInput(ort),
|
||||||
|
sanitizeInput(webseite), sanitizeInput(geburtsdatum)]
|
||||||
|
);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Einrichtung/persoenlich:', error);
|
||||||
|
res.status(500).json({ error: 'Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basis-Unterlage (Anschreiben/Lebenslauf/…) je Typ upserten. Leerinhalt
|
||||||
|
// löscht den Eintrag nicht, sondern speichert ihn als leeren Text — so bleibt
|
||||||
|
// der Typ beim erneuten Öffnen sichtbar.
|
||||||
|
app.post('/api/einrichtung/basis', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const typ = String(req.body.typ || '').trim();
|
||||||
|
if (!typ) return res.status(400).json({ error: 'Typ fehlt' });
|
||||||
|
const inhalt = req.body.inhalt != null ? String(req.body.inhalt) : '';
|
||||||
|
const existing = await dbGet('SELECT id FROM basis_dokumente WHERE user_id = ? AND typ = ?', [uid(), typ]);
|
||||||
|
if (existing) {
|
||||||
|
await dbRun('UPDATE basis_dokumente SET inhalt = ? WHERE id = ? AND user_id = ?',
|
||||||
|
[inhalt, existing.id, uid()]);
|
||||||
|
} else {
|
||||||
|
await dbRun('INSERT INTO basis_dokumente (user_id, typ, name, inhalt) VALUES (?, ?, ?, ?)',
|
||||||
|
[uid(), typ, typ, inhalt]);
|
||||||
|
}
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Einrichtung/basis:', error);
|
||||||
|
res.status(500).json({ error: 'Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ollama-KI: nur die drei Ollama-Keys setzen, ohne die restliche Konfiguration
|
||||||
|
// (Mail, Kalender, API-Token) anzutasten — config.saveAll würde alle nicht
|
||||||
|
// übergebenen Keys leeren, daher gezielt über setForUser.
|
||||||
|
app.post('/api/einrichtung/ollama', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const u = uid();
|
||||||
|
const apiKey = req.body.OLLAMA_API_KEY != null ? String(req.body.OLLAMA_API_KEY).trim() : '';
|
||||||
|
const model = req.body.OLLAMA_MODEL != null ? String(req.body.OLLAMA_MODEL).trim() : '';
|
||||||
|
const host = req.body.OLLAMA_HOST != null ? String(req.body.OLLAMA_HOST).trim() : '';
|
||||||
|
if (host) {
|
||||||
|
try { await assertSafeUrl(host); }
|
||||||
|
catch (vErr) { return res.status(400).json({ error: 'Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
|
||||||
|
}
|
||||||
|
await config.setForUser(u, 'OLLAMA_API_KEY', apiKey);
|
||||||
|
if (model) await config.setForUser(u, 'OLLAMA_MODEL', model);
|
||||||
|
if (host) await config.setForUser(u, 'OLLAMA_HOST', host);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Einrichtung/ollama:', error);
|
||||||
|
res.status(500).json({ error: 'Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wizard dauerhaft ausblenden (app_state-Flag), damit er nicht bei jedem
|
||||||
|
// Besuch der leeren Startseite wieder erscheint.
|
||||||
|
app.post('/api/einrichtung/verstecken', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await dbRun(
|
||||||
|
"INSERT INTO app_state (user_id, key, value) VALUES (?, 'state:einrichtung_versteckt', '1') " +
|
||||||
|
"ON CONFLICT(user_id, key) DO UPDATE SET value = '1'",
|
||||||
|
[uid()]
|
||||||
|
);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Einrichtung/verstecken:', error);
|
||||||
|
res.status(500).json({ error: 'Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ----- Indeed import (called by the browser extension) -----
|
// ----- Indeed import (called by the browser extension) -----
|
||||||
app.post('/api/indeed-import', async (req, res) => {
|
app.post('/api/indeed-import', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
|
|
||||||
<main class="flex-1 container mx-auto px-4 py-8">
|
<main class="flex-1 container mx-auto px-4 py-8">
|
||||||
|
<% var zeigeDashboard = !(typeof setupWizard !== 'undefined' && setupWizard && setupWizard.aktiv); %>
|
||||||
|
<% if (!zeigeDashboard) { %>
|
||||||
|
<%- include('partials/einrichtung') %>
|
||||||
|
<% } %>
|
||||||
|
<!-- Dashboard: Markup bleibt immer im DOM, damit main.js seine Listener
|
||||||
|
binden kann (auch der "Bewerbung manuell anlegen"-Pfad des Wizards).
|
||||||
|
Bei aktivem Wizard wird der Block nur ausgeblendet. -->
|
||||||
|
<div id="dashboardWrap" class="<%= zeigeDashboard ? '' : 'hidden' %>">
|
||||||
<!-- Toolbar: search + actions -->
|
<!-- Toolbar: search + actions -->
|
||||||
<div class="flex flex-col gap-3 mb-8 sm:flex-row sm:items-center">
|
<div class="flex flex-col gap-3 mb-8 sm:flex-row sm:items-center">
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
@@ -652,6 +660,7 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
</div><!-- /dashboardWrap -->
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
|||||||
@@ -0,0 +1,518 @@
|
|||||||
|
<%
|
||||||
|
// Einrichtungsassistent für Nutzer, die noch keine Bewerbung angelegt haben.
|
||||||
|
// Server liefert `setupWizard` mit: aktiv, persoenlich (row), anschreiben/lebenslauf
|
||||||
|
// ({id,inhalt}), hasApiKey, ollamaModel, ollamaHost. Das Partial rendert nur den
|
||||||
|
// Wizard; das umgebende index.ejs blendet gleichzeitig das Dashboard aus.
|
||||||
|
var p = setupWizard.persoenlich || {};
|
||||||
|
var anschreibenInhalt = setupWizard.anschreiben ? setupWizard.anschreiben.inhalt : '';
|
||||||
|
var lebenslaufInhalt = setupWizard.lebenslauf ? setupWizard.lebenslauf.inhalt : '';
|
||||||
|
%>
|
||||||
|
<style>
|
||||||
|
/* Fortschrittsanzeige: Verbindungslinie zwischen den Schritt-Punkten. */
|
||||||
|
.einr-steps { position: relative; }
|
||||||
|
.einr-steps::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.einr-step-dot {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.einr-step.is-active .einr-step-dot {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
/* Sanftes Einblenden der Schrittinhalte. */
|
||||||
|
.einr-panel { animation: einrFade 0.28s ease; }
|
||||||
|
@keyframes einrFade {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.einr-panel { animation: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<section id="einrichtung" class="mb-10">
|
||||||
|
<div class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-gray-200/80 bg-white shadow-xl shadow-gray-900/5 dark:border-white/10 dark:bg-gray-800">
|
||||||
|
|
||||||
|
<!-- Kopfbereich mit Verlaufsanzeige -->
|
||||||
|
<div class="relative bg-gradient-to-br from-sky-500 via-blue-600 to-indigo-600 px-6 py-7 text-white sm:px-8">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span class="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-white/15 ring-1 ring-white/25 backdrop-blur">
|
||||||
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.293-.24.438-.613.43-.991a7.723 7.723 0 010-.255c.008-.379-.137-.75-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.28z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-semibold leading-tight sm:text-2xl">NextJobs einrichten</h1>
|
||||||
|
<p class="mt-1 max-w-md text-sm text-white/80">
|
||||||
|
In wenigen Schritten bist du startklar für deine erste Bewerbung.
|
||||||
|
Du kannst jeden Schritt überspringen und später nachholen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="einrAusblenden"
|
||||||
|
class="shrink-0 rounded-lg px-3 py-1.5 text-xs font-medium text-white/80 ring-1 ring-white/25 transition hover:bg-white/10 hover:text-white">
|
||||||
|
Ausblenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schrittanzeige -->
|
||||||
|
<ol id="einrSteps" class="einr-steps mt-7 flex items-center justify-between gap-1">
|
||||||
|
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="1">
|
||||||
|
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">1</span>
|
||||||
|
<span class="text-[11px] font-medium text-white/80">Willkommen</span>
|
||||||
|
</li>
|
||||||
|
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="2">
|
||||||
|
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">2</span>
|
||||||
|
<span class="text-[11px] font-medium text-white/80">Profil</span>
|
||||||
|
</li>
|
||||||
|
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="3">
|
||||||
|
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">3</span>
|
||||||
|
<span class="text-[11px] font-medium text-white/80">Vorlagen</span>
|
||||||
|
</li>
|
||||||
|
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="4">
|
||||||
|
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">4</span>
|
||||||
|
<span class="text-[11px] font-medium text-white/80">KI</span>
|
||||||
|
</li>
|
||||||
|
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="5">
|
||||||
|
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">5</span>
|
||||||
|
<span class="text-[11px] font-medium text-white/80">Abschluss</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular / Schrittinhalte -->
|
||||||
|
<form id="einrForm" class="px-6 py-6 sm:px-8" autocomplete="off" novalidate>
|
||||||
|
|
||||||
|
<!-- Schritt 1: Willkommen -->
|
||||||
|
<div class="einr-panel" data-panel="1">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Herzlich willkommen!</h2>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
|
||||||
|
NextJobs begleitet dich vom Stellenimport bis zur fertigen Bewerbung — mit
|
||||||
|
automatisch zugeschnittenen Anschreiben und Lebenslauf. Damit das klappt,
|
||||||
|
hinterlegen wir zuerst deine Kontaktdaten, deine Basis-Unterlagen und den
|
||||||
|
KI-Zugang. Das dauert etwa drei Minuten.
|
||||||
|
</p>
|
||||||
|
<ul class="mt-5 space-y-2.5 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<li class="flex items-start gap-2.5">
|
||||||
|
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
<span><strong class="font-medium text-gray-800 dark:text-white">Persönliche Angaben</strong> — fließen in jede Bewerbung ein.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2.5">
|
||||||
|
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
<span><strong class="font-medium text-gray-800 dark:text-white">Basis-Unterlagen</strong> — dein Standard-Anschreiben & Lebenslauf als Text.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2.5">
|
||||||
|
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
<span><strong class="font-medium text-gray-800 dark:text-white">Ollama-KI</strong> — erzeugt daraus später die passenden Unterlagen.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schritt 2: Persönliche Angaben -->
|
||||||
|
<div class="einr-panel hidden" data-panel="2">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Persönliche Angaben</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Diese Daten stehen oben in jedem Anschreiben und Lebenslauf. Pflichtfeld ist nur der Name.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="einr-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Vollständiger Name <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="einr-name" name="name" required
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Max Mustermann" value="<%= p.name || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-adresse" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Adresse</label>
|
||||||
|
<textarea id="einr-adresse" name="adresse" rows="2"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Musterstraße 1, 12345 Musterstadt"><%= p.adresse || '' %></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label for="einr-email" class="block text-xs font-medium text-gray-500 dark:text-gray-400">E-Mail</label>
|
||||||
|
<input type="email" id="einr-email" name="email"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="name@example.com" value="<%= p.email || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-telefon" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Telefon</label>
|
||||||
|
<input type="text" id="einr-telefon" name="telefon"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="z. B. 030 12345678" value="<%= p.telefon || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-ort" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Wohnort</label>
|
||||||
|
<input type="text" id="einr-ort" name="ort"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="z. B. Berlin" value="<%= p.ort || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-kundennummer" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Jobcenter-Kundennummer</label>
|
||||||
|
<input type="text" id="einr-kundennummer" name="kundennummer"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="z. B. 123456789" value="<%= p.kundennummer || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-webseite" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Webseite</label>
|
||||||
|
<input type="text" id="einr-webseite" name="webseite"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="z. B. nextjobs.cc" value="<%= p.webseite || '' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-geburtsdatum" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Geburtsdatum</label>
|
||||||
|
<input type="text" id="einr-geburtsdatum" name="geburtsdatum"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="TT.MM.JJJJ" value="<%= p.geburtsdatum || '' %>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schritt 3: Basis-Unterlagen -->
|
||||||
|
<div class="einr-panel hidden" data-panel="3">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Basis-Unterlagen</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Füge dein Standard-Anschreiben und deinen Lebenslauf als Text ein. Die KI
|
||||||
|
nutzt sie als Faktengrundlage und erfindet nichts hinzu. Du kannst auch
|
||||||
|
schon vorhandene Texte einfach hereinkopieren.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 space-y-5">
|
||||||
|
<div>
|
||||||
|
<label for="einr-anschreiben" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Standard-Anschreiben</label>
|
||||||
|
<textarea id="einr-anschreiben" rows="6"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm leading-relaxed text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Sehr geehrte Damen und Herren, …"><%= anschreibenInhalt %></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-lebenslauf" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Lebenslauf</label>
|
||||||
|
<textarea id="einr-lebenslauf" rows="8"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm leading-relaxed text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Persönliche Daten, Berufserfahrung, Ausbildung, Kenntnisse …"><%= lebenslaufInhalt %></textarea>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Tipp: Ausführliche Vorlagen lassen sich später unter „Vorlagen“ verfeinern —
|
||||||
|
inkl. Design, Foto und weiteren Doktypen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schritt 4: Ollama-KI -->
|
||||||
|
<div class="einr-panel hidden" data-panel="4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Ollama-KI einrichten</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Über die Ollama Cloud erzeugt NextJobs automatisch zugeschnittene
|
||||||
|
Bewerbungsunterlagen. Ohne API-Schlüssel bleibt dieser Schritt deaktiviert —
|
||||||
|
du kannst ihn auch später unter „Einstellungen“ nachholen.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="einr-apikey" class="block text-sm font-medium text-gray-700 dark:text-gray-300">API-Schlüssel</label>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<input type="password" id="einr-apikey" name="OLLAMA_API_KEY"
|
||||||
|
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="ol-…" autocomplete="off">
|
||||||
|
<button type="button" id="einr-toggle-key"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||||
|
title="Wert anzeigen/verbergen" tabindex="-1">
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Schlüssel holen unter
|
||||||
|
<a href="https://ollama.com/settings/keys" target="_blank" rel="noopener" class="text-blue-600 hover:underline dark:text-sky-400">ollama.com/settings/keys</a>.
|
||||||
|
<% if (setupWizard.hasApiKey) { %>
|
||||||
|
<span class="ml-1 text-green-600 dark:text-green-400">· bereits hinterlegt</span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label for="einr-model" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Modell</label>
|
||||||
|
<input type="text" id="einr-model" name="OLLAMA_MODEL"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
value="<%= setupWizard.ollamaModel || 'gpt-oss:120b' %>">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="einr-host" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Host</label>
|
||||||
|
<input type="text" id="einr-host" name="OLLAMA_HOST"
|
||||||
|
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
value="<%= setupWizard.ollamaHost || 'https://ollama.com' %>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schritt 5: Abschluss -->
|
||||||
|
<div class="einr-panel hidden" data-panel="5">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Fast geschafft!</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Hier ist dein Stand. Fehlende Punkte kannst du jederzeit unter
|
||||||
|
„Vorlagen“ oder „Einstellungen“ nachholen.
|
||||||
|
</p>
|
||||||
|
<ul id="einrStatus" class="mt-5 divide-y divide-gray-100 dark:divide-gray-700/60 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<li class="flex items-center gap-3 px-4 py-3" data-check="profil">
|
||||||
|
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">Persönliche Angaben</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 px-4 py-3" data-check="anschreiben">
|
||||||
|
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">Standard-Anschreiben hinterlegt</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 px-4 py-3" data-check="lebenslauf">
|
||||||
|
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">Lebenslauf hinterlegt</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3 px-4 py-3" data-check="ki">
|
||||||
|
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-200">Ollama-KI aktiv</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-5 rounded-lg bg-blue-50 p-4 text-sm text-blue-800 dark:bg-blue-500/10 dark:text-blue-200">
|
||||||
|
<p class="font-medium">Empfehlung: automatische Stellensuche einrichten</p>
|
||||||
|
<p class="mt-1 text-blue-700/90 dark:text-blue-200/80">
|
||||||
|
Lege unter „Stellensuche → Suchprofil & Läufe“ fest, wo und wonach
|
||||||
|
automatisch gesucht wird. So landen passende Stellenangebote direkt hier.
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<a href="/jobsuche" class="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-blue-700">Suchprofil öffnen</a>
|
||||||
|
<a href="/vorlagen" class="inline-flex items-center gap-1.5 rounded-md bg-white px-3 py-1.5 text-xs font-medium text-blue-600 ring-1 ring-blue-200 transition hover:bg-blue-50 dark:bg-transparent dark:text-blue-200 dark:ring-blue-400/40 dark:hover:bg-white/5">Vorlagen verfeinern</a>
|
||||||
|
<a href="/einstellungen" class="inline-flex items-center gap-1.5 rounded-md bg-white px-3 py-1.5 text-xs font-medium text-blue-600 ring-1 ring-blue-200 transition hover:bg-blue-50 dark:bg-transparent dark:text-blue-200 dark:ring-blue-400/40 dark:hover:bg-white/5">Alle Einstellungen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fuß-Navigation -->
|
||||||
|
<div class="mt-8 flex flex-wrap items-center justify-between gap-3 border-t border-gray-200 pt-5 dark:border-gray-700">
|
||||||
|
<button type="button" id="einrZurueck"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md px-3.5 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" id="einrDirekt"
|
||||||
|
class="hidden text-sm font-medium text-blue-600 hover:underline dark:text-sky-400">
|
||||||
|
Bewerbung manuell anlegen
|
||||||
|
</button>
|
||||||
|
<button type="button" id="einrUeberspringen"
|
||||||
|
class="rounded-md px-3.5 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||||
|
Überspringen
|
||||||
|
</button>
|
||||||
|
<button type="button" id="einrWeiter"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60">
|
||||||
|
<span id="einrWeiterLabel">Los geht's</span>
|
||||||
|
<svg id="einrWeiterIcon" class="h-4 w-4" 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"/></svg>
|
||||||
|
<svg id="einrSpinner" class="hidden h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="einrFehler" class="mt-3 hidden text-sm text-red-600 dark:text-red-400"></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var form = document.getElementById('einrForm');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
var step = 1;
|
||||||
|
var MAX = 5;
|
||||||
|
var panels = {};
|
||||||
|
Array.prototype.slice.call(form.querySelectorAll('.einr-panel')).forEach(function (el) {
|
||||||
|
panels[el.getAttribute('data-panel')] = el;
|
||||||
|
});
|
||||||
|
var stepEls = Array.prototype.slice.call(document.querySelectorAll('#einrSteps .einr-step'));
|
||||||
|
|
||||||
|
var btnZurueck = document.getElementById('einrZurueck');
|
||||||
|
var btnUeberspringen = document.getElementById('einrUeberspringen');
|
||||||
|
var btnWeiter = document.getElementById('einrWeiter');
|
||||||
|
var lblWeiter = document.getElementById('einrWeiterLabel');
|
||||||
|
var iconWeiter = document.getElementById('einrWeiterIcon');
|
||||||
|
var spinner = document.getElementById('einrSpinner');
|
||||||
|
var btnDirekt = document.getElementById('einrDirekt');
|
||||||
|
var btnAusblenden = document.getElementById('einrAusblenden');
|
||||||
|
var fehler = document.getElementById('einrFehler');
|
||||||
|
|
||||||
|
var labels = { 1: 'Los geht\'s', 2: 'Weiter', 3: 'Weiter', 4: 'Weiter', 5: 'Abschließen' };
|
||||||
|
|
||||||
|
function showFehler(msg) {
|
||||||
|
fehler.textContent = msg || '';
|
||||||
|
fehler.classList.toggle('hidden', !msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBusy(busy) {
|
||||||
|
btnWeiter.disabled = busy;
|
||||||
|
spinner.classList.toggle('hidden', !busy);
|
||||||
|
iconWeiter.classList.toggle('hidden', busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
Object.keys(panels).forEach(function (n) {
|
||||||
|
panels[n].classList.toggle('hidden', String(n) !== String(step));
|
||||||
|
});
|
||||||
|
stepEls.forEach(function (el) {
|
||||||
|
var n = Number(el.getAttribute('data-step'));
|
||||||
|
var dot = el.querySelector('.einr-step-dot');
|
||||||
|
el.classList.toggle('is-active', n === step);
|
||||||
|
if (n < step) {
|
||||||
|
dot.style.backgroundColor = 'rgba(255,255,255,0.92)';
|
||||||
|
dot.style.color = '#1d4ed8';
|
||||||
|
dot.innerHTML = '<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>';
|
||||||
|
} else if (n === step) {
|
||||||
|
dot.style.backgroundColor = '#ffffff';
|
||||||
|
dot.style.color = '#1d4ed8';
|
||||||
|
dot.textContent = String(n);
|
||||||
|
} else {
|
||||||
|
dot.style.backgroundColor = '';
|
||||||
|
dot.style.color = '';
|
||||||
|
dot.textContent = String(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnZurueck.classList.toggle('hidden', step === 1);
|
||||||
|
btnUeberspringen.classList.toggle('hidden', step === 1 || step === MAX);
|
||||||
|
btnDirekt.classList.toggle('hidden', step !== 1 && step !== MAX);
|
||||||
|
lblWeiter.textContent = labels[step] || 'Weiter';
|
||||||
|
}
|
||||||
|
|
||||||
|
function gehZu(n) {
|
||||||
|
step = Math.max(1, Math.min(MAX, n));
|
||||||
|
if (step === MAX) aktualiereStatus();
|
||||||
|
render();
|
||||||
|
// Oberkante sichtbar halten, damit der nächste Schritt nicht unter dem
|
||||||
|
// sticky Header verschwindet.
|
||||||
|
var sec = document.getElementById('einrichtung');
|
||||||
|
if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abschluss-Checkliste live aus den Formularwerten berechnen.
|
||||||
|
function aktualiereStatus() {
|
||||||
|
var vals = {
|
||||||
|
profil: !!(form.querySelector('#einr-name') || {}).value,
|
||||||
|
anschreiben: !!(form.querySelector('#einr-anschreiben') || {}).value,
|
||||||
|
lebenslauf: !!(form.querySelector('#einr-lebenslauf') || {}).value,
|
||||||
|
ki: !!(form.querySelector('#einr-apikey') || {}).value
|
||||||
|
};
|
||||||
|
if (!vals.ki) vals.ki = <%= setupWizard.hasApiKey ? 'true' : 'false' %>;
|
||||||
|
Object.keys(vals).forEach(function (k) {
|
||||||
|
var li = form.querySelector('[data-check="' + k + '"]');
|
||||||
|
if (!li) return;
|
||||||
|
var ico = li.querySelector('.einr-ico');
|
||||||
|
var ok = !!vals[k];
|
||||||
|
ico.style.backgroundColor = ok ? 'rgba(16,185,129,0.12)' : 'rgba(245,158,11,0.12)';
|
||||||
|
ico.style.color = ok ? '#059669' : '#d97706';
|
||||||
|
ico.innerHTML = ok
|
||||||
|
? '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>'
|
||||||
|
: '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01"/></svg>';
|
||||||
|
li.style.color = ok ? '' : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(url, data) {
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
Object.keys(data).forEach(function (k) { body.append(k, data[k]); });
|
||||||
|
return fetch(url, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' } })
|
||||||
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichert den Inhalt des aktuellen Schritts vor dem Weiterspringen.
|
||||||
|
function speichern(stepNr) {
|
||||||
|
if (stepNr === 2) {
|
||||||
|
if (!form.querySelector('#einr-name').value.trim()) {
|
||||||
|
showFehler('Bitte gib mindestens deinen Namen ein — das ist das einzige Pflichtfeld.');
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
return post('/api/einrichtung/persoenlich', {
|
||||||
|
name: form.querySelector('#einr-name').value,
|
||||||
|
adresse: form.querySelector('#einr-adresse').value,
|
||||||
|
kundennummer: form.querySelector('#einr-kundennummer').value,
|
||||||
|
email: form.querySelector('#einr-email').value,
|
||||||
|
telefon: form.querySelector('#einr-telefon').value,
|
||||||
|
ort: form.querySelector('#einr-ort').value,
|
||||||
|
webseite: form.querySelector('#einr-webseite').value,
|
||||||
|
geburtsdatum: form.querySelector('#einr-geburtsdatum').value
|
||||||
|
}).then(function (r) {
|
||||||
|
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (stepNr === 3) {
|
||||||
|
var a = form.querySelector('#einr-anschreiben').value;
|
||||||
|
var l = form.querySelector('#einr-lebenslauf').value;
|
||||||
|
return post('/api/einrichtung/basis', { typ: 'Anschreiben', inhalt: a })
|
||||||
|
.then(function () { return post('/api/einrichtung/basis', { typ: 'Lebenslauf', inhalt: l }); })
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (stepNr === 4) {
|
||||||
|
return post('/api/einrichtung/ollama', {
|
||||||
|
OLLAMA_API_KEY: form.querySelector('#einr-apikey').value,
|
||||||
|
OLLAMA_MODEL: form.querySelector('#einr-model').value,
|
||||||
|
OLLAMA_HOST: form.querySelector('#einr-host').value
|
||||||
|
}).then(function (r) {
|
||||||
|
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
btnWeiter.addEventListener('click', function () {
|
||||||
|
showFehler('');
|
||||||
|
if (step === MAX) {
|
||||||
|
// Assistent abschließen: dauerhaft ausblenden und zur Übersicht.
|
||||||
|
post('/api/einrichtung/verstecken', {}).then(function () { window.location.href = '/'; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
speichern(step).then(function () {
|
||||||
|
setBusy(false);
|
||||||
|
gehZu(step + 1);
|
||||||
|
}).catch(function (e) {
|
||||||
|
setBusy(false);
|
||||||
|
if (e && e.message) showFehler(e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnZurueck.addEventListener('click', function () { showFehler(''); gehZu(step - 1); });
|
||||||
|
|
||||||
|
btnUeberspringen.addEventListener('click', function () {
|
||||||
|
showFehler('');
|
||||||
|
// Überspringen speichert bewusst nicht — der Schritt gilt als offen.
|
||||||
|
gehZu(step + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDirekt.addEventListener('click', function () {
|
||||||
|
if (typeof window.openAddApplicationModal === 'function') window.openAddApplicationModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnAusblenden.addEventListener('click', function () {
|
||||||
|
post('/api/einrichtung/verstecken', {}).then(function () { window.location.href = '/'; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// API-Schlüssel anzeigen/verbergen.
|
||||||
|
var toggle = document.getElementById('einr-toggle-key');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
var inp = document.getElementById('einr-apikey');
|
||||||
|
if (inp) inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user