Achivement System
This commit is contained in:
406
routes/api.js
406
routes/api.js
@@ -886,6 +886,15 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
|
||||
[player_id, location_id, recorded_time, new Date()]
|
||||
);
|
||||
|
||||
// Achievement-Überprüfung nach Zeit-Eingabe
|
||||
try {
|
||||
await pool.query('SELECT check_all_achievements($1)', [player_id]);
|
||||
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
|
||||
} catch (achievementError) {
|
||||
console.error('Fehler bei Achievement-Check:', achievementError);
|
||||
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WebSocket-Event senden für Live-Updates
|
||||
@@ -1202,7 +1211,10 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
|
||||
ORDER BY t.created_at DESC
|
||||
`, [player_id]);
|
||||
|
||||
res.json(timesResult.rows);
|
||||
res.json({
|
||||
success: true,
|
||||
data: timesResult.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Benutzerzeiten:', error);
|
||||
@@ -1213,6 +1225,47 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/public/user-player/{supabase_user_id}:
|
||||
* get:
|
||||
* summary: Spieler-Info anhand der Supabase User ID abrufen
|
||||
* description: Ruft Spieler-Informationen für das Dashboard ab
|
||||
* tags: [Public API]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: supabase_user_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* description: Supabase User ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Spieler-Informationen
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Player'
|
||||
* 404:
|
||||
* description: Spieler nicht gefunden
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Server-Fehler
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// Get player info by Supabase user ID (no auth required for dashboard)
|
||||
router.get('/v1/public/user-player/:supabase_user_id', async (req, res) => {
|
||||
const { supabase_user_id } = req.params;
|
||||
@@ -1685,6 +1738,75 @@ router.get('/v1/public/locations', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/public/times:
|
||||
* get:
|
||||
* summary: Alle Zeiten mit Standort-Informationen abrufen
|
||||
* description: Ruft alle aufgezeichneten Zeiten mit Standort-Details ab
|
||||
* tags: [Public API]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: location
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Filter nach Standort-ID
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 100
|
||||
* description: Maximale Anzahl der Ergebnisse
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Liste aller Zeiten
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* player_id:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* location_id:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* recorded_time:
|
||||
* type: object
|
||||
* properties:
|
||||
* seconds:
|
||||
* type: number
|
||||
* minutes:
|
||||
* type: number
|
||||
* milliseconds:
|
||||
* type: number
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* location_name:
|
||||
* type: string
|
||||
* latitude:
|
||||
* type: number
|
||||
* longitude:
|
||||
* type: number
|
||||
* 500:
|
||||
* description: Server-Fehler
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// Public route to get times for location with parameter
|
||||
router.get('/v1/public/times', async (req, res) => {
|
||||
const { location } = req.query;
|
||||
@@ -2022,6 +2144,15 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
|
||||
[player_id, location_id, timeInterval]
|
||||
);
|
||||
|
||||
// Achievement-Überprüfung nach Zeit-Eingabe
|
||||
try {
|
||||
await pool.query('SELECT check_all_achievements($1)', [player_id]);
|
||||
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
|
||||
} catch (achievementError) {
|
||||
console.error('Fehler bei Achievement-Check:', achievementError);
|
||||
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Lauf erfolgreich hinzugefügt',
|
||||
@@ -2169,4 +2300,277 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ACHIEVEMENT ENDPOINTS ====================
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/achievements:
|
||||
* get:
|
||||
* summary: Alle verfügbaren Achievements abrufen
|
||||
* description: Ruft alle aktiven Achievements im System ab
|
||||
* tags: [Achievements]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Liste aller Achievements
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Achievement'
|
||||
* 500:
|
||||
* description: Server-Fehler
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// Get all achievements
|
||||
router.get('/achievements', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT id, name, description, category, icon, points, is_active
|
||||
FROM achievements
|
||||
WHERE is_active = true
|
||||
ORDER BY category, points DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching achievements:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Achievements'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/achievements/player/{playerId}:
|
||||
* get:
|
||||
* summary: Achievements eines Spielers abrufen
|
||||
* description: Ruft alle Achievements für einen bestimmten Spieler ab
|
||||
* tags: [Achievements]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: playerId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* format: uuid
|
||||
* description: Eindeutige Spieler-ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Liste der Spieler-Achievements
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/Achievement'
|
||||
* - $ref: '#/components/schemas/PlayerAchievement'
|
||||
* 500:
|
||||
* description: Server-Fehler
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
// Get player achievements
|
||||
router.get('/achievements/player/:playerId', async (req, res) => {
|
||||
try {
|
||||
const { playerId } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.id,
|
||||
a.name,
|
||||
a.description,
|
||||
a.category,
|
||||
a.icon,
|
||||
a.points,
|
||||
pa.progress,
|
||||
pa.is_completed,
|
||||
pa.earned_at
|
||||
FROM achievements a
|
||||
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
||||
WHERE a.is_active = true
|
||||
ORDER BY
|
||||
pa.is_completed DESC,
|
||||
a.category,
|
||||
a.points DESC
|
||||
`, [playerId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching player achievements:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Spieler-Achievements'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get player achievement statistics
|
||||
router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
||||
try {
|
||||
const { playerId } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
COUNT(pa.id) as total_achievements,
|
||||
COUNT(CASE WHEN pa.is_completed = true THEN 1 END) as completed_achievements,
|
||||
SUM(CASE WHEN pa.is_completed = true THEN a.points ELSE 0 END) as total_points,
|
||||
COUNT(CASE WHEN pa.is_completed = true AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE THEN 1 END) as achievements_today
|
||||
FROM achievements a
|
||||
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
|
||||
WHERE a.is_active = true
|
||||
`, [playerId]);
|
||||
|
||||
const stats = result.rows[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_achievements: parseInt(stats.total_achievements),
|
||||
completed_achievements: parseInt(stats.completed_achievements),
|
||||
total_points: parseInt(stats.total_points) || 0,
|
||||
achievements_today: parseInt(stats.achievements_today),
|
||||
completion_percentage: stats.total_achievements > 0 ?
|
||||
Math.round((stats.completed_achievements / stats.total_achievements) * 100) : 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching player achievement stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Achievement-Statistiken'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check achievements for a specific player
|
||||
router.post('/achievements/check/:playerId', async (req, res) => {
|
||||
try {
|
||||
const { playerId } = req.params;
|
||||
|
||||
// Verify player exists
|
||||
const playerCheck = await pool.query('SELECT id FROM players WHERE id = $1', [playerId]);
|
||||
if (playerCheck.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Spieler nicht gefunden'
|
||||
});
|
||||
}
|
||||
|
||||
// Run achievement check
|
||||
await pool.query('SELECT check_all_achievements($1)', [playerId]);
|
||||
|
||||
// Get newly earned achievements
|
||||
const newAchievements = await pool.query(`
|
||||
SELECT a.name, a.description, a.icon, a.points
|
||||
FROM player_achievements pa
|
||||
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||
WHERE pa.player_id = $1
|
||||
AND pa.is_completed = true
|
||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||
ORDER BY pa.earned_at DESC
|
||||
`, [playerId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Achievement-Check abgeschlossen',
|
||||
data: {
|
||||
new_achievements: newAchievements.rows,
|
||||
count: newAchievements.rows.length
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking achievements:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Überprüfen der Achievements'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Run daily achievement check for all players
|
||||
router.post('/achievements/daily-check', async (req, res) => {
|
||||
try {
|
||||
// This endpoint runs the daily achievement check
|
||||
const { runDailyAchievements } = require('../scripts/daily_achievements');
|
||||
|
||||
await runDailyAchievements();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Tägliche Achievement-Überprüfung abgeschlossen'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running daily achievement check:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler bei der täglichen Achievement-Überprüfung'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get achievement leaderboard
|
||||
router.get('/achievements/leaderboard', async (req, res) => {
|
||||
try {
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
p.firstname,
|
||||
p.lastname,
|
||||
COUNT(pa.id) as completed_achievements,
|
||||
SUM(a.points) as total_points
|
||||
FROM players p
|
||||
INNER JOIN player_achievements pa ON p.id = pa.player_id
|
||||
INNER JOIN achievements a ON pa.achievement_id = a.id
|
||||
WHERE pa.is_completed = true
|
||||
GROUP BY p.id, p.firstname, p.lastname
|
||||
ORDER BY total_points DESC, completed_achievements DESC
|
||||
LIMIT $1
|
||||
`, [parseInt(limit)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows.map((row, index) => ({
|
||||
rank: index + 1,
|
||||
name: `${row.firstname} ${row.lastname}`,
|
||||
completed_achievements: parseInt(row.completed_achievements),
|
||||
total_points: parseInt(row.total_points)
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching achievement leaderboard:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Bestenliste'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router, requireApiKey };
|
||||
|
||||
Reference in New Issue
Block a user