365 lines
9.7 KiB
JavaScript
365 lines
9.7 KiB
JavaScript
const express = require('express');
|
|
const { authenticateToken } = require('../middleware/auth');
|
|
const db = require('../database');
|
|
const { stringify } = require('csv-stringify/sync');
|
|
const { parse } = require('csv-parse/sync');
|
|
|
|
const router = express.Router();
|
|
|
|
// Get all games with optional filters
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const {
|
|
enabled,
|
|
minPlayers,
|
|
maxPlayers,
|
|
playerCount,
|
|
drawing,
|
|
length,
|
|
familyFriendly,
|
|
pack
|
|
} = req.query;
|
|
|
|
let query = 'SELECT * FROM games WHERE 1=1';
|
|
const params = [];
|
|
|
|
if (enabled !== undefined) {
|
|
query += ' AND enabled = ?';
|
|
params.push(enabled === 'true' ? 1 : 0);
|
|
}
|
|
|
|
if (playerCount) {
|
|
const count = parseInt(playerCount);
|
|
query += ' AND min_players <= ? AND max_players >= ?';
|
|
params.push(count, count);
|
|
}
|
|
|
|
if (drawing === 'only') {
|
|
query += ' AND game_type = ?';
|
|
params.push('Drawing');
|
|
} else if (drawing === 'exclude') {
|
|
query += ' AND (game_type != ? OR game_type IS NULL)';
|
|
params.push('Drawing');
|
|
}
|
|
|
|
if (length) {
|
|
if (length === 'short') {
|
|
query += ' AND (length_minutes <= 15 OR length_minutes IS NULL)';
|
|
} else if (length === 'medium') {
|
|
query += ' AND length_minutes > 15 AND length_minutes <= 25';
|
|
} else if (length === 'long') {
|
|
query += ' AND length_minutes > 25';
|
|
}
|
|
}
|
|
|
|
if (familyFriendly !== undefined) {
|
|
query += ' AND family_friendly = ?';
|
|
params.push(familyFriendly === 'true' ? 1 : 0);
|
|
}
|
|
|
|
if (pack) {
|
|
query += ' AND pack_name = ?';
|
|
params.push(pack);
|
|
}
|
|
|
|
query += ' ORDER BY pack_name, title';
|
|
|
|
const stmt = db.prepare(query);
|
|
const games = stmt.all(...params);
|
|
|
|
res.json(games);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get single game by ID
|
|
router.get('/:id', (req, res) => {
|
|
try {
|
|
const game = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
|
|
|
|
if (!game) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
res.json(game);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Create new game (admin only)
|
|
router.post('/', authenticateToken, (req, res) => {
|
|
try {
|
|
const {
|
|
pack_name,
|
|
title,
|
|
min_players,
|
|
max_players,
|
|
length_minutes,
|
|
has_audience,
|
|
family_friendly,
|
|
game_type,
|
|
secondary_type
|
|
} = req.body;
|
|
|
|
if (!pack_name || !title || !min_players || !max_players) {
|
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
}
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO games (
|
|
pack_name, title, min_players, max_players, length_minutes,
|
|
has_audience, family_friendly, game_type, secondary_type
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
const result = stmt.run(
|
|
pack_name,
|
|
title,
|
|
min_players,
|
|
max_players,
|
|
length_minutes || null,
|
|
has_audience ? 1 : 0,
|
|
family_friendly ? 1 : 0,
|
|
game_type || null,
|
|
secondary_type || null
|
|
);
|
|
|
|
const newGame = db.prepare('SELECT * FROM games WHERE id = ?').get(result.lastInsertRowid);
|
|
res.status(201).json(newGame);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Update game (admin only)
|
|
router.put('/:id', authenticateToken, (req, res) => {
|
|
try {
|
|
const {
|
|
pack_name,
|
|
title,
|
|
min_players,
|
|
max_players,
|
|
length_minutes,
|
|
has_audience,
|
|
family_friendly,
|
|
game_type,
|
|
secondary_type,
|
|
enabled
|
|
} = req.body;
|
|
|
|
const stmt = db.prepare(`
|
|
UPDATE games SET
|
|
pack_name = COALESCE(?, pack_name),
|
|
title = COALESCE(?, title),
|
|
min_players = COALESCE(?, min_players),
|
|
max_players = COALESCE(?, max_players),
|
|
length_minutes = ?,
|
|
has_audience = COALESCE(?, has_audience),
|
|
family_friendly = COALESCE(?, family_friendly),
|
|
game_type = ?,
|
|
secondary_type = ?,
|
|
enabled = COALESCE(?, enabled)
|
|
WHERE id = ?
|
|
`);
|
|
|
|
const result = stmt.run(
|
|
pack_name || null,
|
|
title || null,
|
|
min_players || null,
|
|
max_players || null,
|
|
length_minutes !== undefined ? length_minutes : null,
|
|
has_audience !== undefined ? (has_audience ? 1 : 0) : null,
|
|
family_friendly !== undefined ? (family_friendly ? 1 : 0) : null,
|
|
game_type !== undefined ? game_type : null,
|
|
secondary_type !== undefined ? secondary_type : null,
|
|
enabled !== undefined ? (enabled ? 1 : 0) : null,
|
|
req.params.id
|
|
);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
const updatedGame = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
|
|
res.json(updatedGame);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Delete game (admin only)
|
|
router.delete('/:id', authenticateToken, (req, res) => {
|
|
try {
|
|
const stmt = db.prepare('DELETE FROM games WHERE id = ?');
|
|
const result = stmt.run(req.params.id);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
res.json({ message: 'Game deleted successfully' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Toggle game enabled status (admin only)
|
|
router.patch('/:id/toggle', authenticateToken, (req, res) => {
|
|
try {
|
|
const game = db.prepare('SELECT enabled FROM games WHERE id = ?').get(req.params.id);
|
|
|
|
if (!game) {
|
|
return res.status(404).json({ error: 'Game not found' });
|
|
}
|
|
|
|
const newStatus = game.enabled === 1 ? 0 : 1;
|
|
db.prepare('UPDATE games SET enabled = ? WHERE id = ?').run(newStatus, req.params.id);
|
|
|
|
const updatedGame = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
|
|
res.json(updatedGame);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get list of unique pack names
|
|
router.get('/meta/packs', (req, res) => {
|
|
try {
|
|
const packs = db.prepare(`
|
|
SELECT
|
|
pack_name,
|
|
COUNT(*) as game_count,
|
|
SUM(enabled) as enabled_count
|
|
FROM games
|
|
GROUP BY pack_name
|
|
ORDER BY pack_name
|
|
`).all();
|
|
|
|
res.json(packs);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Toggle entire pack (admin only)
|
|
router.patch('/packs/:name/toggle', authenticateToken, (req, res) => {
|
|
try {
|
|
const { enabled } = req.body;
|
|
|
|
if (enabled === undefined) {
|
|
return res.status(400).json({ error: 'enabled status required' });
|
|
}
|
|
|
|
const stmt = db.prepare('UPDATE games SET enabled = ? WHERE pack_name = ?');
|
|
const result = stmt.run(enabled ? 1 : 0, req.params.name);
|
|
|
|
res.json({
|
|
message: `Pack ${enabled ? 'enabled' : 'disabled'} successfully`,
|
|
gamesAffected: result.changes
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Export games to CSV (admin only)
|
|
router.get('/export/csv', authenticateToken, (req, res) => {
|
|
try {
|
|
const games = db.prepare('SELECT * FROM games ORDER BY pack_name, title').all();
|
|
|
|
const csvData = games.map(game => ({
|
|
'Game Pack': game.pack_name,
|
|
'Game Title': game.title,
|
|
'Min. Players': game.min_players,
|
|
'Max. Players': game.max_players,
|
|
'Length': game.length_minutes ? `${game.length_minutes} minutes` : '????',
|
|
'Audience': game.has_audience ? 'Yes' : 'No',
|
|
'Family Friendly?': game.family_friendly ? 'Yes' : 'No',
|
|
'Game Type': game.game_type || '',
|
|
'Secondary Type': game.secondary_type || ''
|
|
}));
|
|
|
|
const csv = stringify(csvData, { header: true });
|
|
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=games-export.csv');
|
|
res.send(csv);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Import games from CSV (admin only)
|
|
router.post('/import/csv', authenticateToken, (req, res) => {
|
|
try {
|
|
const { csvData, mode } = req.body; // mode: 'append' or 'replace'
|
|
|
|
if (!csvData) {
|
|
return res.status(400).json({ error: 'CSV data required' });
|
|
}
|
|
|
|
const records = parse(csvData, {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
trim: true
|
|
});
|
|
|
|
if (mode === 'replace') {
|
|
db.prepare('DELETE FROM games').run();
|
|
}
|
|
|
|
const insert = db.prepare(`
|
|
INSERT INTO games (
|
|
pack_name, title, min_players, max_players, length_minutes,
|
|
has_audience, family_friendly, game_type, secondary_type, enabled
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
`);
|
|
|
|
const insertMany = db.transaction((games) => {
|
|
for (const game of games) {
|
|
insert.run(
|
|
game['Game Pack'],
|
|
game['Game Title'],
|
|
parseInt(game['Min. Players']) || 1,
|
|
parseInt(game['Max. Players']) || 8,
|
|
parseLengthMinutes(game['Length']),
|
|
parseBoolean(game['Audience']),
|
|
parseBoolean(game['Family Friendly?']),
|
|
game['Game Type'] || null,
|
|
game['Secondary Type'] || null
|
|
);
|
|
}
|
|
});
|
|
|
|
insertMany(records);
|
|
|
|
res.json({
|
|
message: `Successfully imported ${records.length} games`,
|
|
count: records.length,
|
|
mode
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
function parseLengthMinutes(lengthStr) {
|
|
if (!lengthStr || lengthStr === '????' || lengthStr === '?') {
|
|
return null;
|
|
}
|
|
const match = lengthStr.match(/(\d+)/);
|
|
return match ? parseInt(match[1]) : null;
|
|
}
|
|
|
|
function parseBoolean(value) {
|
|
if (!value || value === '?' || value === '????') {
|
|
return 0;
|
|
}
|
|
return value.toLowerCase() === 'yes' ? 1 : 0;
|
|
}
|
|
|
|
module.exports = router;
|
|
|