Jobangebote: Favoriten (Stern) und Startseiten-Sektion ganz oben

Stellen lassen sich per Stern merken — unabhängig vom Status (offen /
uebernommen / abgelehnt). Neue Spalte jobangebote.favorit (PRAGMA-Migration),
JSON-Toggle-Endpunkt /jobangebote/:id/favorit, Stern-Button auf den Karten
sowie eine "Gemerkte Stellen"-Sektion ganz oben auf der Startseite. Toggle
laeuft per fetch mit optimistischem UI; beim Entfernen auf der Startseite
verschwindet die Zeile sofort.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 14:49:47 +02:00
co-authored by Claude
parent 2566dcc98d
commit 3a108d3fec
7 changed files with 210 additions and 1 deletions
+39 -1
View File
@@ -1438,6 +1438,16 @@ async function initializeDatabase() {
await exec('CREATE INDEX IF NOT EXISTS idx_jobangebote_url_norm ON jobangebote(url_norm)');
await exec('CREATE INDEX IF NOT EXISTS idx_jobangebote_firma_slug ON jobangebote(firma_slug)');
// Favoriten-Merkmal (Stern): 0 = nicht gemerkt, 1 = favorisiert. Favoriten
// erscheinen oben auf der Startseite — unabhängig vom Status (offen /
// uebernommen / abgelehnt). Spalte wird nur bei Bedarf angelegt.
{
const jobSpalten = await dbAll('PRAGMA table_info(jobangebote)');
if (!jobSpalten.some((s) => s.name === 'favorit')) {
await exec('ALTER TABLE jobangebote ADD COLUMN favorit INTEGER NOT NULL DEFAULT 0');
}
}
// Blacklist of job offers that must never (re)appear in the list (per user).
await exec(`
CREATE TABLE IF NOT EXISTS jobangebote_blacklist (
@@ -1744,6 +1754,17 @@ initializeDatabase().then(async () => {
};
}
// Gemerkte Stellen (Favoriten) für die Sektion ganz oben auf der
// Startseite — unabhängig vom Angebot-Status (offen/uebernommen/…).
const favoriten = await dbAll(
`SELECT j.id, j.firma, j.stelle, j.ort, j.status, j.quelle_url,
j.verknuepfte_bewerbung_id, j.gehalt, j.favorit
FROM jobangebote j
WHERE j.user_id = ? AND j.favorit = 1
ORDER BY j.updated_at DESC, j.id DESC`,
[U]
);
res.render('index', {
applications,
statistics: {
@@ -1760,7 +1781,8 @@ initializeDatabase().then(async () => {
artOptions: ART_OPTIONS,
statusOptions: STATUS_OPTIONS,
labelOptions: LABEL_OPTIONS,
setupWizard
setupWizard,
favoriten
});
} catch (error) {
console.error('Error:', error);
@@ -3602,6 +3624,22 @@ initializeDatabase().then(async () => {
}
});
// Favorit umschalten (Stern). JSON-Antwort, damit der Client den Stern
// sofort umschalten kann, ohne die Seite neu zu laden. Favoriten sind
// unabhängig vom Angebot-Status sichtbar — auch „uebernommen“ bleibt merkbar.
app.post('/jobangebote/:id/favorit', async (req, res) => {
try {
const angebot = await dbGet('SELECT favorit FROM jobangebote WHERE id = ? AND user_id = ?', [req.params.id, uid()]);
if (!angebot) return res.status(404).json({ error: 'Jobangebot nicht gefunden' });
const neu = angebot.favorit ? 0 : 1;
await dbRun('UPDATE jobangebote SET favorit = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?', [neu, req.params.id, uid()]);
res.json({ success: true, favorit: neu });
} catch (error) {
console.error('Error toggling favorite:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
// Blacklist management page — see what is blocked and remove entries.
app.get('/blacklist', async (req, res) => {
try {