import { Router, Request, Response } from 'express'; import { query } from '../db/client'; import { logger } from '../services/logger'; const router = Router(); export interface Achievement { id: string; name: string; description: string; icon: string; // Material Symbol name color: string; // CSS color for the glow rankLabel: string; // e.g. "Gold-Rang", "On Fire" unlocked: boolean; progress: number; // 0-100 percentage current: number; // current value target: number; // target value } router.get('/', async (req: Request, res: Response): Promise => { const userId = req.staffbaseUser?.sub; if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; } try { // Get user stats const statsResult = await query( `SELECT total_points, tips_count, exact_count, tendency_count, rank FROM leaderboard WHERE user_id = $1`, [userId] ); const stats = statsResult[0]; const exactCount = parseInt(String(stats?.exact_count ?? '0')); const tipsCount = parseInt(String(stats?.tips_count ?? '0')); const rank = stats?.rank ? parseInt(String(stats.rank)) : null; // Calculate streak (same logic as dashboard) const pastMatches = await query<{ has_tip: boolean }>( `SELECT CASE WHEN t.id IS NOT NULL THEN true ELSE false END AS has_tip FROM matches m LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1 WHERE m.status IN ('FINISHED', 'IN_PLAY') ORDER BY m.utc_date DESC`, [userId] ); let maxStreak = 0; let currentStreak = 0; for (const m of pastMatches) { if (m.has_tip === true || (m.has_tip as unknown) === 't') { currentStreak++; if (currentStreak > maxStreak) maxStreak = currentStreak; } else { currentStreak = 0; } } // Check early tipper: tips submitted >24h before kickoff const earlyTips = await query<{ count: string }>( `SELECT COUNT(*) AS count FROM tips t JOIN matches m ON m.id = t.match_id WHERE t.user_id = $1 AND m.status = 'FINISHED' AND t.created_at < m.utc_date - interval '24 hours'`, [userId] ); const earlyCount = parseInt(earlyTips[0]?.count || '0'); const finishedWithTips = await query<{ count: string }>( `SELECT COUNT(*) AS count FROM tips t JOIN matches m ON m.id = t.match_id WHERE t.user_id = $1 AND m.status = 'FINISHED'`, [userId] ); const finishedTipCount = parseInt(finishedWithTips[0]?.count || '0'); // Check globetrotter: tips in all groups const groupsTipped = await query<{ count: string }>( `SELECT COUNT(DISTINCT m.group_name) AS count FROM tips t JOIN matches m ON m.id = t.match_id WHERE t.user_id = $1 AND m.stage = 'GROUP_STAGE' AND m.group_name IS NOT NULL`, [userId] ); const groupCount = parseInt(groupsTipped[0]?.count || '0'); const totalGroups = 12; // WM 2026 has 12 groups (A-L) // Build achievements const achievements: Achievement[] = [ { id: 'sharpshooter', name: 'Scharfschütze', description: '5 exakte Tipps', icon: 'target', color: '#f5ce53', rankLabel: 'Gold-Rang', unlocked: exactCount >= 5, progress: Math.min(100, (exactCount / 5) * 100), current: exactCount, target: 5, }, { id: 'streak_master', name: 'Serien-Tipper', description: '10er Tipp-Serie', icon: 'local_fire_department', color: '#ff716c', rankLabel: 'On Fire', unlocked: maxStreak >= 10, progress: Math.min(100, (maxStreak / 10) * 100), current: maxStreak, target: 10, }, { id: 'league_leader', name: 'Tabellenführer', description: 'Platz 1 erreicht', icon: 'crown', color: '#e6c047', rankLabel: 'Prestige', unlocked: rank === 1, progress: rank === 1 ? 100 : 0, current: rank === 1 ? 1 : 0, target: 1, }, { id: 'early_bird', name: 'Frühtipper', description: 'Tipps 24h vor Anpfiff', icon: 'alarm', color: '#4BB7F8', rankLabel: 'Speedster', unlocked: finishedTipCount > 0 && earlyCount === finishedTipCount, progress: finishedTipCount > 0 ? Math.min(100, (earlyCount / finishedTipCount) * 100) : 0, current: earlyCount, target: finishedTipCount || 1, }, { id: 'globetrotter', name: 'Globetrotter', description: 'Alle Gruppenspiele', icon: 'public', color: '#4BB7F8', rankLabel: 'Reisender', unlocked: groupCount >= totalGroups, progress: Math.min(100, (groupCount / totalGroups) * 100), current: groupCount, target: totalGroups, }, { id: 'diamond', name: 'Diamant', description: '20 exakte Tipps', icon: 'diamond', color: '#a066ff', rankLabel: 'Legendär', unlocked: exactCount >= 20, progress: Math.min(100, (exactCount / 20) * 100), current: exactCount, target: 20, }, ]; const unlockedCount = achievements.filter(a => a.unlocked).length; res.json({ achievements, unlockedCount, total: achievements.length }); } catch (error) { logger.error('Achievements failed', { error }); res.status(500).json({ error: 'Internal server error' }); } }); export default router;