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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user