164 lines
5.3 KiB
JavaScript
164 lines
5.3 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const db = require('./db');
|
||
|
|
const { requireAdmin, isValidAdminKey } = require('./auth');
|
||
|
|
|
||
|
|
const router = express.Router();
|
||
|
|
|
||
|
|
function todayStr() {
|
||
|
|
const d = new Date();
|
||
|
|
const y = d.getFullYear();
|
||
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
|
|
const day = String(d.getDate()).padStart(2, '0');
|
||
|
|
return `${y}-${m}-${day}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||
|
|
|
||
|
|
// Resolves a caller-supplied date, falling back to today and rejecting
|
||
|
|
// malformed or future dates (returns null on invalid input).
|
||
|
|
function resolveDate(raw) {
|
||
|
|
if (!raw) return todayStr();
|
||
|
|
if (!DATE_RE.test(raw)) return null;
|
||
|
|
if (raw > todayStr()) return null;
|
||
|
|
return raw;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- Players -----------------------------------------------------------
|
||
|
|
|
||
|
|
router.get('/players', (req, res) => {
|
||
|
|
const date = resolveDate(req.query.date) || todayStr();
|
||
|
|
const rows = db
|
||
|
|
.prepare(
|
||
|
|
`SELECT p.id, p.name,
|
||
|
|
COUNT(d.id) AS total,
|
||
|
|
SUM(CASE WHEN d.date = ? THEN 1 ELSE 0 END) AS dateCount
|
||
|
|
FROM players p
|
||
|
|
LEFT JOIN doses d ON d.player_id = p.id
|
||
|
|
GROUP BY p.id
|
||
|
|
ORDER BY total DESC, p.name COLLATE NOCASE ASC`
|
||
|
|
)
|
||
|
|
.all(date);
|
||
|
|
res.json(rows.map((r) => ({ ...r, dateCount: r.dateCount || 0 })));
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/players', requireAdmin, (req, res) => {
|
||
|
|
const name = String(req.body?.name || '').trim();
|
||
|
|
if (!name) {
|
||
|
|
return res.status(400).json({ error: 'Le nom est requis' });
|
||
|
|
}
|
||
|
|
if (name.length > 40) {
|
||
|
|
return res.status(400).json({ error: 'Le nom est trop long (40 caractères maximum)' });
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const info = db.prepare('INSERT INTO players (name) VALUES (?)').run(name);
|
||
|
|
res.status(201).json({ id: info.lastInsertRowid, name, total: 0, dateCount: 0 });
|
||
|
|
} catch (err) {
|
||
|
|
if (err.code === 'ERR_SQLITE_ERROR' && /UNIQUE constraint failed/.test(err.message)) {
|
||
|
|
return res.status(409).json({ error: 'Un joueur avec ce nom existe déjà' });
|
||
|
|
}
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/players/:id', requireAdmin, (req, res) => {
|
||
|
|
const playerId = Number(req.params.id);
|
||
|
|
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
|
||
|
|
if (!player) {
|
||
|
|
return res.status(404).json({ error: 'Joueur introuvable' });
|
||
|
|
}
|
||
|
|
db.exec('BEGIN');
|
||
|
|
db.prepare('DELETE FROM doses WHERE player_id = ?').run(playerId);
|
||
|
|
db.prepare('DELETE FROM players WHERE id = ?').run(playerId);
|
||
|
|
db.exec('COMMIT');
|
||
|
|
res.status(204).end();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---- Timeseries (doses per player, per date) -----------------------------
|
||
|
|
|
||
|
|
router.get('/timeseries', (req, res) => {
|
||
|
|
const players = db.prepare('SELECT id, name FROM players ORDER BY id ASC').all();
|
||
|
|
const dates = db
|
||
|
|
.prepare('SELECT DISTINCT date FROM doses ORDER BY date ASC')
|
||
|
|
.all()
|
||
|
|
.map((r) => r.date);
|
||
|
|
|
||
|
|
const counts = db
|
||
|
|
.prepare('SELECT player_id, date, COUNT(*) AS c FROM doses GROUP BY player_id, date')
|
||
|
|
.all();
|
||
|
|
|
||
|
|
const byPlayerDate = new Map(); // `${playerId}|${date}` -> count
|
||
|
|
for (const row of counts) {
|
||
|
|
byPlayerDate.set(`${row.player_id}|${row.date}`, row.c);
|
||
|
|
}
|
||
|
|
|
||
|
|
const series = players.map((p) => ({
|
||
|
|
playerId: p.id,
|
||
|
|
name: p.name,
|
||
|
|
counts: dates.map((date) => byPlayerDate.get(`${p.id}|${date}`) || 0),
|
||
|
|
}));
|
||
|
|
|
||
|
|
res.json({ dates, series });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---- Admin auth ----------------------------------------------------------
|
||
|
|
|
||
|
|
router.post('/admin/verify', (req, res) => {
|
||
|
|
const password = String(req.body?.password || '');
|
||
|
|
res.json({ ok: isValidAdminKey(password) });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---- Doses (admin only) --------------------------------------------------
|
||
|
|
|
||
|
|
router.post('/doses', requireAdmin, (req, res) => {
|
||
|
|
const playerId = Number(req.body?.playerId);
|
||
|
|
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
|
||
|
|
if (!player) {
|
||
|
|
return res.status(404).json({ error: 'Joueur introuvable' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const date = resolveDate(req.body?.date);
|
||
|
|
if (!date) {
|
||
|
|
return res.status(400).json({ error: 'Date invalide (elle ne peut pas être dans le futur)' });
|
||
|
|
}
|
||
|
|
|
||
|
|
db.prepare('INSERT INTO doses (player_id, date) VALUES (?, ?)').run(playerId, date);
|
||
|
|
|
||
|
|
const total = db.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ?').get(playerId).c;
|
||
|
|
const dateCount = db
|
||
|
|
.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ? AND date = ?')
|
||
|
|
.get(playerId, date).c;
|
||
|
|
|
||
|
|
res.status(201).json({ playerId, total, dateCount });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/doses/latest', requireAdmin, (req, res) => {
|
||
|
|
const playerId = Number(req.query.playerId);
|
||
|
|
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
|
||
|
|
if (!player) {
|
||
|
|
return res.status(404).json({ error: 'Joueur introuvable' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const date = resolveDate(req.query.date);
|
||
|
|
if (!date) {
|
||
|
|
return res.status(400).json({ error: 'Date invalide (elle ne peut pas être dans le futur)' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const latest = db
|
||
|
|
.prepare('SELECT id FROM doses WHERE player_id = ? AND date = ? ORDER BY id DESC LIMIT 1')
|
||
|
|
.get(playerId, date);
|
||
|
|
|
||
|
|
if (!latest) {
|
||
|
|
return res.status(404).json({ error: 'Aucune dose enregistrée pour ce joueur à cette date' });
|
||
|
|
}
|
||
|
|
db.prepare('DELETE FROM doses WHERE id = ?').run(latest.id);
|
||
|
|
|
||
|
|
const total = db.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ?').get(playerId).c;
|
||
|
|
const dateCount = db
|
||
|
|
.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ? AND date = ?')
|
||
|
|
.get(playerId, date).c;
|
||
|
|
|
||
|
|
res.json({ playerId, total, dateCount });
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|