Refactor monster_catches from table to view and add overwrite option

- Convert monster_catches from table to view for automatic calculation
- Add overwrite checkbox to monster import UI
- Remove manual INSERT/UPDATE logic for catches (now handled by view)
- Simplify API endpoints to query view instead of managing state
- Add confirmation dialog for overwrite operation

Benefits:
- No data duplication
- Always accurate catch status based on current steps
- Simpler codebase with less state management
- Easier to reset steps without orphaned catch records

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-20 16:08:02 +02:00
parent 05e4a505b3
commit 04891bf449
4 changed files with 106 additions and 108 deletions

View File

@@ -55,18 +55,24 @@ function initializeDatabase() {
)
`);
// Monster catches table
// Monster catches view - derived from actual step data
// Drop the old table if it exists and recreate as a view
db.run(`DROP TABLE IF EXISTS monster_catches`);
db.run(`
CREATE TABLE IF NOT EXISTS monster_catches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id INTEGER NOT NULL,
monster_id INTEGER NOT NULL,
caught_at DATETIME DEFAULT CURRENT_TIMESTAMP,
final_steps INTEGER NOT NULL,
FOREIGN KEY (team_id) REFERENCES teams(id),
FOREIGN KEY (monster_id) REFERENCES daily_monsters(id),
UNIQUE(team_id, monster_id)
)
CREATE VIEW IF NOT EXISTS monster_catches AS
SELECT
dm.id as monster_id,
dm.date,
t.id as team_id,
t.name as team_name,
COALESCE(SUM(ds.steps), 0) as final_steps,
dm.step_goal
FROM daily_monsters dm
CROSS JOIN teams t
LEFT JOIN participants p ON t.id = p.team_id
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = dm.date
GROUP BY dm.id, dm.date, t.id, t.name, dm.step_goal
HAVING final_steps >= dm.step_goal
`);
// Create indexes for better query performance
@@ -74,8 +80,6 @@ function initializeDatabase() {
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_participants_team ON participants(team_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_monsters_date ON daily_monsters(date)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_team ON monster_catches(team_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_monster ON monster_catches(monster_id)`);
console.log('Database initialized successfully');
});

View File

@@ -352,54 +352,17 @@ app.get('/api/monsters/timeline', async (req, res) => {
try {
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
// For each monster, check if teams caught it and update catches table
// For each monster, get catches from the view
const timeline = await Promise.all(monsters.map(async (monster) => {
// Get team totals for this monster's date
const teamTotals = await allQuery(`
SELECT
t.id as team_id,
t.name as team_name,
COALESCE(SUM(ds.steps), 0) as total_steps
FROM teams t
LEFT JOIN participants p ON t.id = p.team_id
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ?
GROUP BY t.id, t.name
`, [monster.date]);
// Check and record catches for teams that met the goal
for (const team of teamTotals) {
if (team.total_steps >= monster.step_goal) {
// Check if catch already recorded
const existingCatch = await getQuery(
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
[team.team_id, monster.id]
);
if (!existingCatch) {
// Record the catch
await runQuery(
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
[team.team_id, monster.id, team.total_steps]
);
} else {
// Update the final_steps if it changed
await runQuery(
'UPDATE monster_catches SET final_steps = ? WHERE team_id = ? AND monster_id = ?',
[team.total_steps, team.team_id, monster.id]
);
}
}
}
// Now get the updated catches
const catches = await allQuery(`
SELECT
mc.*,
t.name as team_name
FROM monster_catches mc
JOIN teams t ON mc.team_id = t.id
WHERE mc.monster_id = ?
ORDER BY mc.caught_at
monster_id,
team_id,
team_name,
final_steps
FROM monster_catches
WHERE monster_id = ?
ORDER BY final_steps DESC
`, [monster.id]);
return {
@@ -430,7 +393,7 @@ app.get('/api/monsters/:date', async (req, res) => {
// Add/import monsters (bulk CSV import)
app.post('/api/monsters/import', async (req, res) => {
try {
const { data } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
const { data, overwrite } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
if (!data || !Array.isArray(data)) {
return res.status(400).json({ error: 'Invalid data format' });
@@ -438,8 +401,16 @@ app.post('/api/monsters/import', async (req, res) => {
let monstersCreated = 0;
let monstersUpdated = 0;
let monstersDeleted = 0;
let errors = [];
// If overwrite is true, delete all existing monsters first
// Note: monster_catches is now a view, so no need to delete from it
if (overwrite === true) {
const deleteMonstersResult = await runQuery('DELETE FROM daily_monsters');
monstersDeleted = deleteMonstersResult.changes || 0;
}
for (let i = 0; i < data.length; i++) {
const row = data[i];
@@ -449,38 +420,54 @@ app.post('/api/monsters/import', async (req, res) => {
continue;
}
// Check if monster already exists for this date
const existing = await getQuery(
'SELECT id FROM daily_monsters WHERE date = ?',
[row.date]
);
if (existing) {
// Update existing monster
await runQuery(
'UPDATE daily_monsters SET monster_name = ?, monster_description = ?, step_goal = ?, monster_icon = ? WHERE date = ?',
[row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹', row.date]
);
monstersUpdated++;
} else {
// Create new monster
if (overwrite) {
// When overwriting, always insert (since we deleted everything)
await runQuery(
'INSERT INTO daily_monsters (date, monster_name, monster_description, step_goal, monster_icon) VALUES (?, ?, ?, ?, ?)',
[row.date, row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹']
);
monstersCreated++;
} else {
// Check if monster already exists for this date
const existing = await getQuery(
'SELECT id FROM daily_monsters WHERE date = ?',
[row.date]
);
if (existing) {
// Update existing monster
await runQuery(
'UPDATE daily_monsters SET monster_name = ?, monster_description = ?, step_goal = ?, monster_icon = ? WHERE date = ?',
[row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹', row.date]
);
monstersUpdated++;
} else {
// Create new monster
await runQuery(
'INSERT INTO daily_monsters (date, monster_name, monster_description, step_goal, monster_icon) VALUES (?, ?, ?, ?, ?)',
[row.date, row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹']
);
monstersCreated++;
}
}
} catch (error) {
errors.push(`Row ${i + 1}: ${error.message}`);
}
}
res.json({
const result = {
success: true,
monstersCreated,
monstersUpdated,
errors: errors.length > 0 ? errors : null
});
};
if (overwrite) {
result.monstersDeleted = monstersDeleted;
} else {
result.monstersUpdated = monstersUpdated;
}
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -500,12 +487,13 @@ app.get('/api/monsters/:date/catches', async (req, res) => {
const catches = await allQuery(`
SELECT
mc.*,
t.name as team_name
FROM monster_catches mc
JOIN teams t ON mc.team_id = t.id
WHERE mc.monster_id = ?
ORDER BY mc.caught_at
monster_id,
team_id,
team_name,
final_steps
FROM monster_catches
WHERE monster_id = ?
ORDER BY final_steps DESC
`, [monster.id]);
res.json(catches);
@@ -534,6 +522,7 @@ app.get('/api/monsters/status/today', async (req, res) => {
}
// Get team progress for yesterday's date
// The view automatically calculates which teams caught it
const progress = await allQuery(`
SELECT
t.id as team_id,
@@ -547,25 +536,6 @@ app.get('/api/monsters/status/today', async (req, res) => {
ORDER BY total_steps DESC
`, [monster.step_goal, yesterday]);
// Check and record new catches
for (const team of progress) {
if (team.caught) {
// Check if catch already recorded
const existingCatch = await getQuery(
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
[team.team_id, monster.id]
);
if (!existingCatch) {
// Record the catch
await runQuery(
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
[team.team_id, monster.id, team.total_steps]
);
}
}
}
res.json({ monster, progress });
} catch (error) {
res.status(500).json({ error: error.message });