Termine: alle anstehenden auf Startseite + Bearbeiten; Status Telefonat

- Startseite zeigt alle anstehenden Termine (vorher auf 6 begrenzt)
- Termine lassen sich inline bearbeiten (CalDAV-Update, Neu-Anlage bei 404/412)
- Neuer Bewerbungsstatus Telefonat (Listen, Farben, API, PDF, Statistik)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:27:19 +02:00
co-authored by Claude
parent ce5fa1eab1
commit d9ba8698e0
9 changed files with 184 additions and 14 deletions
+84 -3
View File
@@ -62,7 +62,7 @@ const ART_OPTIONS = [
];
const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung'
];
@@ -862,6 +862,16 @@ async function autoBlacklistOffer(offer, grund) {
// Upcoming appointments (not yet ended), newest first, for the dashboard widget.
async function upcomingTermine(limit = 6) {
const now = new Date().toISOString();
// limit = null/0 ⇒ kein Limit, alle anstehenden Termine (z. B. Startseite).
if (!limit) {
return dbAll(
`SELECT t.*, b.firma AS bewerbung_firma, b.stelle AS bewerbung_stelle
FROM termine t LEFT JOIN bewerbungen b ON b.id = t.bewerbung_id
WHERE t.user_id = ? AND COALESCE(t.ende, t.start) >= ?
ORDER BY t.start ASC`,
[currentUserId(), now]
);
}
return dbAll(
`SELECT t.*, b.firma AS bewerbung_firma, b.stelle AS bewerbung_stelle
FROM termine t LEFT JOIN bewerbungen b ON b.id = t.bewerbung_id
@@ -1779,7 +1789,7 @@ initializeDatabase().then(async () => {
await attachVerlauf(applications);
applications.forEach((a) => { a.labelsArr = parseLabels(a.labels); });
// Upcoming calendar appointments for the dashboard widget.
const kommendeTermine = await upcomingTermine(6);
const kommendeTermine = await upcomingTermine(null);
// Get statistics
const totalCount = await dbGet('SELECT COUNT(*) as count FROM bewerbungen WHERE user_id = ?', [U]);
@@ -2013,7 +2023,7 @@ initializeDatabase().then(async () => {
// Any reply at all — everything past "Gesendet" except "Keine Rückmeldung".
const rueckStatus = [
'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert',
'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert', 'Telefonat',
'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung'
@@ -2532,6 +2542,13 @@ initializeDatabase().then(async () => {
const basisAnhaenge = await dbAll('SELECT id, name, dateiname FROM basis_anhaenge WHERE user_id = ? ORDER BY id ASC', [U]);
// Calendar appointments for this application (mirrored to SOGo).
const termine = await dbAll('SELECT * FROM termine WHERE bewerbung_id = ? AND user_id = ? ORDER BY start ASC', [id, U]);
// Vorausgefüllte Werte für die Bearbeiten-Formulare (Berliner Wanduhr).
termine.forEach((t) => {
const s = caldav.utcToWallInputs(new Date(t.start));
t.editDatum = s.datum;
t.editVon = t.ganztags ? '09:00' : s.zeit;
t.editBis = (!t.ganztags && t.ende) ? caldav.utcToWallInputs(new Date(t.ende)).zeit : '';
});
// E-Mail correspondence (sent + received), oldest first, with attachments.
const emails = await dbAll(
@@ -3019,6 +3036,70 @@ initializeDatabase().then(async () => {
}
});
// Termin bearbeiten: aktualisiert den Kalender-Termin (CalDAV) und das
// lokale Spiegelbild. Legt einen neuen CalDAV-Eintrag an, falls der alte
// entfernt wurde (z. B. remote gelöscht).
app.post('/bewerbung/:id/termine/:tid/edit', async (req, res) => {
const { id, tid } = req.params;
try {
const t = await dbGet('SELECT * FROM termine WHERE id = ? AND bewerbung_id = ? AND user_id = ?', [tid, id, uid()]);
if (!t) return res.redirect('/bewerbung/' + id + '#termine');
if (!caldav.isConfigured()) {
return res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent('Kalender ist nicht konfiguriert.') + '#termine');
}
const b = req.body || {};
if (!b.datum) {
return res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent('Bitte ein Datum angeben.') + '#termine');
}
const ganztags = b.ganztags === 'on' || b.ganztags === '1' || b.ganztags === 'true';
const typ = b.typ === 'vorstellungsgespraech' ? 'vorstellungsgespraech' : 'termin';
const titel = (b.titel || '').trim() || (typ === 'vorstellungsgespraech' ? 'Vorstellungsgespräch' : 'Termin');
const erinnerung = Math.max(0, parseInt(b.erinnerung_min, 10) || 0);
let start, ende = null;
if (ganztags) {
const [y, mo, d] = String(b.datum).split('-').map(Number);
start = caldav.wallToUtc(y, mo, d, 0, 0);
} else {
start = caldav.localInputToUtc(b.datum, b.von || '09:00');
if (b.bis) ende = caldav.localInputToUtc(b.datum, b.bis);
}
const evt = {
summary: titel, location: b.ort || '', description: b.notiz || '',
start, end: ende, allDay: ganztags, alarmMin: erinnerung,
};
let href = t.caldav_href, etag = t.caldav_etag, caldavUid = t.caldav_uid;
if (href) {
const upd = await caldav.updateEvent({ href, etag, ...evt }).catch((e) => {
// 412/404 bedeutet: der entfernte Termin wurde zwischenzeitlich
// geändert oder gelöscht — dann legen wir ihn neu an.
if (/HTTP (404|412)/.test(e.message)) return null;
throw e;
});
if (upd) { etag = upd.etag; caldavUid = upd.uid; }
else { const created = await caldav.createEvent(evt); href = created.href; etag = created.etag; caldavUid = created.uid; }
} else {
const created = await caldav.createEvent(evt);
href = created.href; etag = created.etag; caldavUid = created.uid;
}
await dbRun(
`UPDATE termine SET typ = ?, titel = ?, ort = ?, notiz = ?, start = ?, ende = ?, ganztags = ?,
erinnerung_min = ?, caldav_uid = ?, caldav_href = ?, caldav_etag = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?`,
[typ, sanitizeInput(titel), sanitizeInput(b.ort || ''), sanitizeInput(b.notiz || ''),
start.toISOString(), ende ? ende.toISOString() : null, ganztags ? 1 : 0, erinnerung,
caldavUid, href, etag, tid, uid()]
);
res.redirect('/bewerbung/' + id + '?terminok=1#termine');
} catch (error) {
console.error('Error editing termin:', error);
res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent(error.message || 'Termin konnte nicht aktualisiert werden.') + '#termine');
}
});
// Update a timeline entry
app.post('/bewerbung/:id/verlauf/:eintragId', async (req, res) => {
try {