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:
@@ -823,6 +823,7 @@ function renderMonsterTimeline(timeline) {
|
||||
// Monster CSV Import
|
||||
async function importMonsters() {
|
||||
const fileInput = document.getElementById('monster-csv-file');
|
||||
const overwriteCheckbox = document.getElementById('monster-overwrite');
|
||||
const messageDiv = document.getElementById('monster-import-message');
|
||||
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
@@ -830,6 +831,19 @@ async function importMonsters() {
|
||||
return;
|
||||
}
|
||||
|
||||
const overwrite = overwriteCheckbox.checked;
|
||||
|
||||
// Confirm if overwrite is selected
|
||||
if (overwrite) {
|
||||
const confirmed = confirm(
|
||||
'WARNING: This will DELETE all existing monsters and monster catches!\n\n' +
|
||||
'Are you sure you want to proceed?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
@@ -846,18 +860,22 @@ async function importMonsters() {
|
||||
return { date, monster_name, monster_description, step_goal, monster_icon };
|
||||
});
|
||||
|
||||
const result = await apiCall('/api/monsters/import', 'POST', { data });
|
||||
const result = await apiCall('/api/monsters/import', 'POST', { data, overwrite });
|
||||
|
||||
showMessage(
|
||||
messageDiv,
|
||||
`Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`,
|
||||
'success'
|
||||
);
|
||||
let successMessage;
|
||||
if (overwrite) {
|
||||
successMessage = `Success! Deleted ${result.monstersDeleted} old monsters, created ${result.monstersCreated} new monsters`;
|
||||
} else {
|
||||
successMessage = `Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`;
|
||||
}
|
||||
|
||||
showMessage(messageDiv, successMessage, 'success');
|
||||
|
||||
// Reload monsters
|
||||
await loadDailyMonsters();
|
||||
|
||||
fileInput.value = '';
|
||||
overwriteCheckbox.checked = false;
|
||||
} catch (error) {
|
||||
showMessage(messageDiv, `Error: ${error.message}`, 'error');
|
||||
}
|
||||
|
||||
@@ -106,6 +106,12 @@
|
||||
<h3>Admin: Import Monsters</h3>
|
||||
<p class="help-text">Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon</p>
|
||||
<input type="file" id="monster-csv-file" accept=".csv">
|
||||
<div style="margin: 10px 0;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<input type="checkbox" id="monster-overwrite" style="cursor: pointer;">
|
||||
<span style="color: #ff7700;">Overwrite all existing monsters (deletes all current monsters and catches)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button id="import-monsters" class="btn-secondary">Import Monsters</button>
|
||||
<div id="monster-import-message" class="message"></div>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
118
src/server.js
118
src/server.js
@@ -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,6 +420,14 @@ app.post('/api/monsters/import', async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 = ?',
|
||||
@@ -470,17 +449,25 @@ app.post('/api/monsters/import', async (req, res) => {
|
||||
);
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user