Notifications, div fixes, kekse für last location

This commit is contained in:
2025-09-06 12:37:10 +02:00
parent 61d5ef2e6f
commit 8342d95a13
13 changed files with 1325 additions and 39 deletions

View File

@@ -65,6 +65,11 @@ function convertTimeToSeconds(timeStr) {
function convertIntervalToSeconds(interval) {
if (!interval) return 0;
// Handle PostgreSQL interval object format
if (typeof interval === 'object' && interval.seconds !== undefined) {
return parseFloat(interval.seconds);
}
// PostgreSQL interval format: "HH:MM:SS" or "MM:SS" or just seconds
if (typeof interval === 'string') {
const parts = interval.split(':');
@@ -853,30 +858,27 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
if (recordedTimeSeconds < thresholdSeconds) {
// Convert threshold to readable format
const thresholdMinutes = Math.floor(thresholdSeconds / 60);
const thresholdSecs = Math.floor(thresholdSeconds % 60);
const thresholdMs = Math.floor((thresholdSeconds % 1) * 1000);
const thresholdDisplay = thresholdMinutes > 0
? `${thresholdMinutes}:${thresholdSecs.toString().padStart(2, '0')}.${thresholdMs.toString().padStart(3, '0')}`
: `${thresholdSecs}.${thresholdMs.toString().padStart(3, '0')}`;
return res.status(400).json({
success: false,
message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`,
message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${thresholdDisplay} für diesen Standort`,
data: {
recorded_time: recorded_time,
threshold: location.time_threshold,
threshold_display: thresholdDisplay,
location_name: location_name
}
});
}
}
// Prüfen ob Zeit bereits existiert (optional - kann entfernt werden)
const existingTime = await pool.query(
'SELECT id FROM times WHERE player_id = $1 AND location_id = $2 AND recorded_time = $3',
[player_id, location_id, recorded_time]
);
if (existingTime.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'Zeit existiert bereits in der Datenbank'
});
}
// Zeit in Datenbank einfügen
const result = await pool.query(
@@ -2134,6 +2136,38 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
try {
const { player_id, location_id, time_seconds } = req.body;
// Prüfen ob Location existiert und Threshold abrufen
const locationResult = await pool.query(
'SELECT id, name, time_threshold FROM locations WHERE id = $1',
[location_id]
);
if (locationResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Standort nicht gefunden'
});
}
const location = locationResult.rows[0];
// Prüfen ob die Zeit über dem Schwellenwert liegt
if (location.time_threshold) {
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
if (time_seconds < thresholdSeconds) {
return res.status(400).json({
success: false,
message: `Zeit ${time_seconds}s liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`,
data: {
recorded_time: `${time_seconds}s`,
threshold: location.time_threshold,
location_name: location.name
}
});
}
}
// Zeit in INTERVAL konvertieren
const timeInterval = `${time_seconds} seconds`;
@@ -2300,6 +2334,198 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
}
});
// ==================== BEST TIMES ENDPOINTS ====================
/**
* @swagger
* /api/v1/public/best-times:
* get:
* summary: Beste Zeiten abrufen
* description: Ruft die besten Zeiten des Tages, der Woche und des Monats ab
* tags: [Times]
* responses:
* 200:
* description: Beste Zeiten erfolgreich abgerufen
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* daily:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* weekly:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* monthly:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get best times (daily, weekly, monthly)
router.get('/v1/public/best-times', async (req, res) => {
try {
const currentDate = new Date();
const today = currentDate.toISOString().split('T')[0];
const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay()));
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
// Get daily best
const dailyResult = await pool.query(`
WITH daily_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM daily_best
WHERE best_time = (SELECT MIN(best_time) FROM daily_best)
LIMIT 1
`, [today]);
// Get weekly best
const weeklyResult = await pool.query(`
WITH weekly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM weekly_best
WHERE best_time = (SELECT MIN(best_time) FROM weekly_best)
LIMIT 1
`, [weekStart.toISOString().split('T')[0], today]);
// Get monthly best
const monthlyResult = await pool.query(`
WITH monthly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM monthly_best
WHERE best_time = (SELECT MIN(best_time) FROM monthly_best)
LIMIT 1
`, [monthStart.toISOString().split('T')[0], today]);
res.json({
success: true,
data: {
daily: dailyResult.rows[0] || null,
weekly: weeklyResult.rows[0] || null,
monthly: monthlyResult.rows[0] || null
}
});
} catch (error) {
console.error('Error fetching best times:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Bestzeiten',
error: error.message
});
}
});
// ==================== PUSH NOTIFICATION ENDPOINTS ====================
// Subscribe to push notifications
router.post('/v1/public/subscribe', async (req, res) => {
try {
const { endpoint, keys } = req.body;
// Store subscription in database
await pool.query(`
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (player_id)
DO UPDATE SET
endpoint = EXCLUDED.endpoint,
p256dh = EXCLUDED.p256dh,
auth = EXCLUDED.auth,
updated_at = NOW()
`, [
req.session.userId || 'anonymous',
endpoint,
keys.p256dh,
keys.auth
]);
res.json({
success: true,
message: 'Push subscription erfolgreich gespeichert'
});
} catch (error) {
console.error('Error storing push subscription:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Speichern der Push Subscription',
error: error.message
});
}
});
// ==================== ACHIEVEMENT ENDPOINTS ====================
/**
@@ -2396,6 +2622,13 @@ router.get('/achievements', async (req, res) => {
// Get player achievements
router.get('/achievements/player/:playerId', async (req, res) => {
try {
// Set no-cache headers
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
const { playerId } = req.params;
const result = await pool.query(`
@@ -2406,8 +2639,8 @@ router.get('/achievements/player/:playerId', async (req, res) => {
a.category,
a.icon,
a.points,
pa.progress,
pa.is_completed,
COALESCE(pa.progress, 0) as progress,
COALESCE(pa.is_completed, false) as is_completed,
pa.earned_at
FROM achievements a
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
@@ -2434,11 +2667,25 @@ router.get('/achievements/player/:playerId', async (req, res) => {
// Get player achievement statistics
router.get('/achievements/player/:playerId/stats', async (req, res) => {
try {
// Set no-cache headers
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
const { playerId } = req.params;
const result = await pool.query(`
// Get total available achievements
const totalResult = await pool.query(`
SELECT COUNT(*) as total_achievements
FROM achievements
WHERE is_active = true
`);
// Get player's earned achievements
const playerResult = 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
@@ -2447,17 +2694,21 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => {
WHERE a.is_active = true
`, [playerId]);
const stats = result.rows[0];
const totalStats = totalResult.rows[0];
const playerStats = playerResult.rows[0];
const totalAchievements = parseInt(totalStats.total_achievements);
const completedAchievements = parseInt(playerStats.completed_achievements) || 0;
const completionPercentage = totalAchievements > 0 ? Math.round((completedAchievements / totalAchievements) * 100) : 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
total_achievements: totalAchievements,
completed_achievements: completedAchievements,
total_points: parseInt(playerStats.total_points) || 0,
achievements_today: parseInt(playerStats.achievements_today) || 0,
completion_percentage: completionPercentage
}
});
} catch (error) {