Notifications, div fixes, kekse für last location
This commit is contained in:
299
routes/api.js
299
routes/api.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user