diff --git a/public/app.js b/public/app.js index 26d730b..b14c1c5 100644 --- a/public/app.js +++ b/public/app.js @@ -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'); } diff --git a/public/index.html b/public/index.html index dfaf78c..f88775a 100644 --- a/public/index.html +++ b/public/index.html @@ -106,6 +106,12 @@

Admin: Import Monsters

Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon

+
+ +
diff --git a/src/database.js b/src/database.js index 9066cd0..478423d 100644 --- a/src/database.js +++ b/src/database.js @@ -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'); }); diff --git a/src/server.js b/src/server.js index 16244f4..9482df0 100644 --- a/src/server.js +++ b/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,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 });