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 });