diff --git a/server.js b/server.js index ea019c1..02fe5a7 100644 --- a/server.js +++ b/server.js @@ -1707,6 +1707,30 @@ initializeDatabase().then(async () => { ORDER BY yearmonth DESC `, [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', { applications, statistics: { @@ -1722,7 +1746,8 @@ initializeDatabase().then(async () => { caldavTz: caldav.TZ, artOptions: ART_OPTIONS, statusOptions: STATUS_OPTIONS, - labelOptions: LABEL_OPTIONS + labelOptions: LABEL_OPTIONS, + setupWizard }); } catch (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) ----- app.post('/api/indeed-import', async (req, res) => { try { diff --git a/views/index.ejs b/views/index.ejs index 8ab46b5..329760b 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -7,6 +7,14 @@ <%- include('partials/header') %>
+ <% var zeigeDashboard = !(typeof setupWizard !== 'undefined' && setupWizard && setupWizard.aktiv); %> + <% if (!zeigeDashboard) { %> + <%- include('partials/einrichtung') %> + <% } %> + +
@@ -652,6 +660,7 @@ }); })(); +
<%- include('partials/footer') %> diff --git a/views/partials/einrichtung.ejs b/views/partials/einrichtung.ejs new file mode 100644 index 0000000..476e9a3 --- /dev/null +++ b/views/partials/einrichtung.ejs @@ -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 : ''; +%> + + +
+
+ + +
+
+
+ + + + + + +
+

NextJobs einrichten

+

+ In wenigen Schritten bist du startklar für deine erste Bewerbung. + Du kannst jeden Schritt überspringen und später nachholen. +

+
+
+ +
+ + +
    +
  1. + 1 + Willkommen +
  2. +
  3. + 2 + Profil +
  4. +
  5. + 3 + Vorlagen +
  6. +
  7. + 4 + KI +
  8. +
  9. + 5 + Abschluss +
  10. +
+
+ + +
+ + +
+

Herzlich willkommen!

+

+ 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. +

+
    +
  • + + Persönliche Angaben — fließen in jede Bewerbung ein. +
  • +
  • + + Basis-Unterlagen — dein Standard-Anschreiben & Lebenslauf als Text. +
  • +
  • + + Ollama-KI — erzeugt daraus später die passenden Unterlagen. +
  • +
+
+ + + + + + + + + + + + + + +
+ +
+ + + +
+
+ +
+
+
+ + \ No newline at end of file