'use strict'; const express = require('express'); const path = require('path'); const fs = require('fs'); const methodOverride = require('method-override'); const Database = require('better-sqlite3'); const app = express(); const PORT = process.env.PORT || 3000; // ── Database bootstrap ──────────────────────────────────────────────────────── const dataDir = path.join(__dirname, 'data'); if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); const db = new Database(path.join(dataDir, 'bewerbungen.db')); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS bewerbungen ( id INTEGER PRIMARY KEY AUTOINCREMENT, datum DATE NOT NULL, firma TEXT NOT NULL, stelle TEXT NOT NULL, art TEXT, status TEXT, notizen TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY CHECK (id = 1), name TEXT, adresse TEXT, kundennummer TEXT ); INSERT OR IGNORE INTO settings (id) VALUES (1); `); // ── Constants ───────────────────────────────────────────────────────────────── const ART_OPTIONEN = [ 'E-Mail', 'Online-Portal', 'Indeed', 'StepStone', 'Firmenwebsite', 'Post', 'Initiativbewerbung', 'Arbeitsagentur', 'Sonstiges' ]; const STATUS_OPTIONEN = [ 'Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch', 'Absage', 'Einstellung', 'Keine Rückmeldung' ]; const MONATE = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ]; // ── Helpers ─────────────────────────────────────────────────────────────────── function sanitize(val, maxLen = 2000) { if (val === null || val === undefined) return ''; return String(val).trim().slice(0, maxLen); } function safeJson(obj) { return JSON.stringify(obj) .replace(//g, '\\u003e') .replace(/&/g, '\\u0026'); } function getSettings() { return db.prepare('SELECT * FROM settings WHERE id = 1').get() || {}; } // ── Middleware ──────────────────────────────────────────────────────────────── app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(methodOverride('_method')); app.use(express.static(path.join(__dirname, 'public'))); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // ── Routes ──────────────────────────────────────────────────────────────────── // GET / – Übersicht app.get('/', (req, res) => { const now = new Date(); const monat = Math.min(12, Math.max(1, parseInt(req.query.monat) || (now.getMonth() + 1))); const jahr = parseInt(req.query.jahr) || now.getFullYear(); const monatStr = String(monat).padStart(2, '0'); const vonDatum = `${jahr}-${monatStr}-01`; const bisDatum = `${jahr}-${monatStr}-31`; const bewerbungen = db.prepare(` SELECT * FROM bewerbungen WHERE datum BETWEEN ? AND ? ORDER BY datum DESC, id DESC `).all(vonDatum, bisDatum); // Statistics const stats = { gesamt: bewerbungen.length, positiv: 0, absagen: 0, ausstehend: 0 }; for (const b of bewerbungen) { if (b.status === 'Vorstellungsgespräch' || b.status === 'Einstellung') stats.positiv++; else if (b.status === 'Absage') stats.absagen++; else stats.ausstehend++; } // Years for filter dropdown const dbJahre = db.prepare( `SELECT DISTINCT strftime('%Y', datum) AS j FROM bewerbungen ORDER BY j DESC` ).all().map(r => parseInt(r.j)).filter(Boolean); const jahre = [...new Set([...dbJahre, now.getFullYear()])].sort((a, b) => b - a); res.render('index', { bewerbungen, bewerbungenJson: safeJson(bewerbungen), stats, monat, jahr, monate: MONATE, jahre, artOptionen: ART_OPTIONEN, statusOptionen: STATUS_OPTIONEN, settings: getSettings(), monatName: MONATE[monat - 1], fehler: req.query.fehler || null, currentPage: 'uebersicht' }); }); // POST /bewerbungen – Neu anlegen app.post('/bewerbungen', (req, res) => { const { datum, firma, stelle, art, status, notizen } = req.body; if (!datum || !firma || !stelle) { const d = new Date(); return res.redirect(`/?fehler=pflichtfelder&monat=${d.getMonth() + 1}&jahr=${d.getFullYear()}`); } db.prepare(` INSERT INTO bewerbungen (datum, firma, stelle, art, status, notizen) VALUES (?, ?, ?, ?, ?, ?) `).run( sanitize(datum, 20), sanitize(firma), sanitize(stelle), sanitize(art), sanitize(status), sanitize(notizen) ); const d = new Date(datum + 'T00:00:00'); res.redirect(`/?monat=${d.getMonth() + 1}&jahr=${d.getFullYear()}`); }); // PUT /bewerbungen/:id – Aktualisieren app.put('/bewerbungen/:id', (req, res) => { const id = parseInt(req.params.id); const { datum, firma, stelle, art, status, notizen, monat, jahr } = req.body; if (!datum || !firma || !stelle) { return res.redirect(`/?fehler=pflichtfelder&monat=${monat}&jahr=${jahr}`); } db.prepare(` UPDATE bewerbungen SET datum=?, firma=?, stelle=?, art=?, status=?, notizen=?, updated_at=CURRENT_TIMESTAMP WHERE id=? `).run( sanitize(datum, 20), sanitize(firma), sanitize(stelle), sanitize(art), sanitize(status), sanitize(notizen), id ); res.redirect(`/?monat=${monat || 1}&jahr=${jahr || new Date().getFullYear()}`); }); // DELETE /bewerbungen/:id – Löschen app.delete('/bewerbungen/:id', (req, res) => { db.prepare('DELETE FROM bewerbungen WHERE id=?').run(parseInt(req.params.id)); const { monat, jahr } = req.body; const now = new Date(); res.redirect(`/?monat=${monat || now.getMonth() + 1}&jahr=${jahr || now.getFullYear()}`); }); // GET /einstellungen app.get('/einstellungen', (req, res) => { res.render('einstellungen', { settings: getSettings(), gespeichert: req.query.gespeichert === '1', currentPage: 'einstellungen' }); }); // POST /einstellungen app.post('/einstellungen', (req, res) => { const { name, adresse, kundennummer } = req.body; db.prepare(` INSERT OR REPLACE INTO settings (id, name, adresse, kundennummer) VALUES (1, ?, ?, ?) `).run(sanitize(name), sanitize(adresse), sanitize(kundennummer)); res.redirect('/einstellungen?gespeichert=1'); }); // GET /api/pdf-daten – JSON für clientseitige PDF-Generierung app.get('/api/pdf-daten', (req, res) => { const monat = Math.min(12, Math.max(1, parseInt(req.query.monat) || 1)); const jahr = parseInt(req.query.jahr) || new Date().getFullYear(); const ms = String(monat).padStart(2, '0'); const bewerbungen = db.prepare(` SELECT * FROM bewerbungen WHERE datum BETWEEN ? AND ? ORDER BY datum ASC, id ASC `).all(`${jahr}-${ms}-01`, `${jahr}-${ms}-31`); res.json({ bewerbungen, settings: getSettings(), monat, jahr }); }); // ── Start ───────────────────────────────────────────────────────────────────── app.listen(PORT, () => { console.log(`\n✓ Bewerbungs-Tracker läuft auf → http://localhost:${PORT}\n`); });