diff --git a/server.js b/server.js index b81eb21..c711862 100644 --- a/server.js +++ b/server.js @@ -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 { diff --git a/views/index.ejs b/views/index.ejs index 329760b..5a75b91 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -8,6 +8,7 @@
<% var zeigeDashboard = !(typeof setupWizard !== 'undefined' && setupWizard && setupWizard.aktiv); %> + <%- include('partials/favoriten') %> <% if (!zeigeDashboard) { %> <%- include('partials/einrichtung') %> <% } %> diff --git a/views/jobangebote.ejs b/views/jobangebote.ejs index d4774f1..0624b4e 100644 --- a/views/jobangebote.ejs +++ b/views/jobangebote.ejs @@ -139,6 +139,7 @@
+ <%- include('partials/favorit-button', { j: j }) %> <% if (j.verknuepfte_bewerbung_id) { %> @@ -297,5 +298,7 @@ }); })(); + + <%- include('partials/favorit-script') %> \ No newline at end of file diff --git a/views/jobangebote_uebernommen.ejs b/views/jobangebote_uebernommen.ejs index 9aa825e..5f957bc 100644 --- a/views/jobangebote_uebernommen.ejs +++ b/views/jobangebote_uebernommen.ejs @@ -100,6 +100,7 @@
+ <%- include('partials/favorit-button', { j: j }) %> <% if (j.verknuepfte_bewerbung_id) { %> @@ -140,5 +141,7 @@ if (cy) cy.textContent = new Date().getFullYear(); })(); + + <%- include('partials/favorit-script') %> diff --git a/views/partials/favorit-button.ejs b/views/partials/favorit-button.ejs new file mode 100644 index 0000000..04ce97d --- /dev/null +++ b/views/partials/favorit-button.ejs @@ -0,0 +1,16 @@ +<% + // Stern-Button zum Merken/Entfernen eines Jobangebots. Lokale `j` muss das + // Angebot (mindestens id + favorit) sein. Zustand wird über die Klasse + // .is-fav gesteuert (definiert in partials/favorit-script). Das Skript dort + // bedient Klicks per Delegation — also nur einmal pro Seite einbinden. + var favAktiv = !!(typeof j !== 'undefined' && j && j.favorit); +%> + \ No newline at end of file diff --git a/views/partials/favorit-script.ejs b/views/partials/favorit-script.ejs new file mode 100644 index 0000000..39504ff --- /dev/null +++ b/views/partials/favorit-script.ejs @@ -0,0 +1,76 @@ + + \ No newline at end of file diff --git a/views/partials/favoriten.ejs b/views/partials/favoriten.ejs new file mode 100644 index 0000000..719ef1f --- /dev/null +++ b/views/partials/favoriten.ejs @@ -0,0 +1,72 @@ +<% + // Gemerkte Stellen — Sektion ganz oben auf der Startseite. Zeigt alle + // favorisierten Jobangebote unabhängig von ihrem Status (offen/uebernommen/…). + // Wird nur gerendert, wenn mindestens ein Favorit vorhanden ist. + function favStatusBadge(status) { + if (status === 'offen') return 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-200'; + if (status === 'uebernommen') return 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200'; + if (status === 'abgelehnt') return 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-200'; + return 'bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-200'; + } + function favStatusLabel(status) { + if (status === 'offen') return 'Offen'; + if (status === 'uebernommen') return 'Übernommen'; + if (status === 'abgelehnt') return 'Abgelehnt'; + return status || '—'; + } +%> +<% if (favoriten && favoriten.length) { %> +<%- include('favorit-script') %> +
+
+ +

Gemerkte Stellen

+ + <%= favoriten.length %> + +
+
+
+<% } %> \ No newline at end of file