Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
'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, '\\u003c')
|
||||
.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`);
|
||||
});
|
||||
Reference in New Issue
Block a user