feat: Profil-Kategorien + Begrüßung in Zielsprache
languages.greeting (de/en/sv geseedet), neue pair_categories-Tabelle (abgeleitet aus statement- und objektverknüpften Wörtern via word_categories) inkl. Backfill für bereits veröffentlichte Pairs. derivePairCategories() wird beim Publish (pairs + pipeline) aufgerufen. /auth/me liefert language_target_greeting, /auth/stats liefert categories[] mit Punkten je Kategorie fürs Profil. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,8 @@ router.get('/me', requireJwt, async (req, res, next) => {
|
||||
COALESCE(up.daily_goal_ep, 30) AS daily_goal_ep,
|
||||
up.last_practice_at,
|
||||
ln.id AS language_native_id, ln.short_en AS language_native_short, ln.titel_de AS language_native_titel,
|
||||
lt.id AS language_target_id, lt.short_en AS language_target_short, lt.titel_de AS language_target_titel
|
||||
lt.id AS language_target_id, lt.short_en AS language_target_short, lt.titel_de AS language_target_titel,
|
||||
lt.greeting AS language_target_greeting
|
||||
FROM users u
|
||||
LEFT JOIN users_public up ON up.user_id = u.id
|
||||
LEFT JOIN user_names un ON un.id = up.username_id
|
||||
@@ -275,6 +276,22 @@ router.get('/stats', requireJwt, async (req, res, next) => {
|
||||
return { label, value: acc && acc.seen > 0 ? acc.correct / acc.seen : 0, seen: acc?.seen || 0 };
|
||||
});
|
||||
|
||||
// Punkte je Kategorie (Lebensmittel/Tiere/Beruf …) — abgeleitet über pair_categories.
|
||||
// Mehrfach-Kategorien eines Pairs zählen bewusst zu jeder Kategorie.
|
||||
const categoryRows = await query(
|
||||
`SELECT c.id, c.titel_de AS label,
|
||||
COALESCE(SUM(upp.earned_points), 0)::int AS points,
|
||||
COALESCE(SUM(upp.seen_count), 0)::int AS seen
|
||||
FROM user_pair_progress upp
|
||||
JOIN pair_categories pc ON pc.pair_id = upp.pair_id
|
||||
JOIN categories c ON c.id = pc.category_id
|
||||
WHERE upp.user_id = $1
|
||||
GROUP BY c.id, c.titel_de
|
||||
HAVING SUM(upp.earned_points) > 0
|
||||
ORDER BY points DESC`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const t = totals.rows[0] || { pairs_practiced: 0, total_seen: 0, total_correct: 0 };
|
||||
const td = today.rows[0] || { ep: 0, cards: 0, daily_goal_ep: 30 };
|
||||
|
||||
@@ -288,6 +305,7 @@ router.get('/stats', requireJwt, async (req, res, next) => {
|
||||
accuracy: t.total_seen > 0 ? t.total_correct / t.total_seen : 0,
|
||||
},
|
||||
skills,
|
||||
categories: categoryRows.rows,
|
||||
});
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ const { query } = require('../db');
|
||||
const { fillMissingRow } = require('../lib/translate');
|
||||
const { loadPairContext, computeReadiness, loadPairContent, translateWordGroup } = require('../lib/pairContent');
|
||||
const { deletePairDeep } = require('../lib/deleteCascade');
|
||||
const { derivePairCategories } = require('../lib/pairCategories');
|
||||
|
||||
const STATUSES = ['draft', 'reviewed', 'blocked', 'published'];
|
||||
const ANSWER_TYPES = new Set(['yes_no', 'text', 'question', 'word']);
|
||||
@@ -131,6 +132,11 @@ router.patch('/:id', async (req, res, next) => {
|
||||
values
|
||||
);
|
||||
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
// Beim Veröffentlichen Kategorien aus den verknüpften Wörtern ableiten (best effort).
|
||||
if (req.body.status === 'published')
|
||||
await derivePairCategories(result.rows[0].id).catch(() => {});
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
@@ -295,6 +301,8 @@ router.post('/:id/publish', async (req, res, next) => {
|
||||
`UPDATE pairs SET status='published', published_at=COALESCE(published_at,$2) WHERE id=$1 RETURNING *`,
|
||||
[p.id, now]);
|
||||
|
||||
await derivePairCategories(p.id).catch(() => {});
|
||||
|
||||
res.json({ ...upd.rows[0], published_languages: [lang] });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const { enqueue, loadPairs, collectAudioUnits, generateWithBackoff, translatePai
|
||||
const { describeError } = require('./audios');
|
||||
const { PLACEHOLDER_RE, TOKEN_RE, stripLeakedTokens } = require('../lib/placeholders');
|
||||
const { invalidateAudio } = require('../lib/reviewPairs');
|
||||
const { derivePairCategories } = require('../lib/pairCategories');
|
||||
|
||||
// ── Objekt-Wort-Erkennung in Sätzen (für die manuelle Zuweisung beim Review) ──
|
||||
|
||||
@@ -406,6 +407,9 @@ router.post('/picture/:id/publish', async (req, res, next) => {
|
||||
await query(`UPDATE pairs SET status='published', published_at=COALESCE(published_at,$2)
|
||||
WHERE id = ANY($1)`, [pairIds, now]);
|
||||
|
||||
// Kategorien der veröffentlichten Pairs aus ihren Wörtern ableiten (best effort).
|
||||
await derivePairCategories(pairIds).catch(() => {});
|
||||
|
||||
// Verlinkte Wörter: nur 'generated' → 'published' (translated bleibt für die Bild-Generierung
|
||||
// im ServerMonitor-Flow; published würde diesen Schritt überspringen)
|
||||
let publishedWords = 0;
|
||||
|
||||
Reference in New Issue
Block a user