cf64a2dc50
- EPA Agility & Speed: add Max Speed and Max Weighted Speed (mph), decimal record-tracked metrics, per session. - Logged sessions are editable: tapping a category or a logged entry opens it pre-filled; saving updates that entry (PUT /api/entries/:id) instead of creating a duplicate. Record auto-update runs on edit too. - Every value field is now a tap-to-type number input (decimal keypad for speeds) while keeping the +/- stepper buttons; native spinners removed. New decimal metric kind; seed + idempotent migration add the EPA speed metrics. - Bump StartOS package to 0.1.5:0; service worker cache to v5.
122 lines
4.5 KiB
JavaScript
122 lines
4.5 KiB
JavaScript
import { db } from '../db.js';
|
|
|
|
const ISO = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
function ensureDay(day) {
|
|
db.prepare('INSERT OR IGNORE INTO training_days (day) VALUES (?)').run(day);
|
|
}
|
|
|
|
// Write an entry's metric values and bump any personal-best records they beat.
|
|
// Pushes beaten records onto `newRecords`. Used by both create and update.
|
|
function writeValues(entryId, values, newRecords) {
|
|
if (!Array.isArray(values)) return;
|
|
const ins = db.prepare('INSERT INTO entry_values (entry_id, metric_id, value) VALUES (?, ?, ?)');
|
|
for (const v of values) {
|
|
if (!v || v.metric_id == null) continue;
|
|
const metricId = Number(v.metric_id);
|
|
const value = Number(v.value) || 0;
|
|
ins.run(entryId, metricId, value);
|
|
|
|
const m = db.prepare(
|
|
'SELECT id, name, record, higher_is_better, unit FROM category_metrics WHERE id = ? AND track_record = 1'
|
|
).get(metricId);
|
|
if (m) {
|
|
const beats = m.record == null
|
|
|| (m.higher_is_better ? value > m.record : value < m.record);
|
|
if (beats) {
|
|
db.prepare('UPDATE category_metrics SET record = ? WHERE id = ?').run(value, metricId);
|
|
newRecords.push({ metric_id: m.id, name: m.name, unit: m.unit, value, previous: m.record });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function dayPayload(day) {
|
|
const td = db.prepare('SELECT day, notes FROM training_days WHERE day = ?').get(day)
|
|
|| { day, notes: '' };
|
|
const entries = db.prepare(
|
|
'SELECT id, category_id, note, created_at FROM entries WHERE day = ? ORDER BY id'
|
|
).all(day);
|
|
const valuesByEntry = new Map();
|
|
if (entries.length) {
|
|
const ids = entries.map((e) => e.id);
|
|
const rows = db.prepare(
|
|
`SELECT entry_id, metric_id, value FROM entry_values WHERE entry_id IN (${ids.map(() => '?').join(',')})`
|
|
).all(...ids);
|
|
for (const r of rows) {
|
|
if (!valuesByEntry.has(r.entry_id)) valuesByEntry.set(r.entry_id, []);
|
|
valuesByEntry.get(r.entry_id).push({ metric_id: r.metric_id, value: r.value });
|
|
}
|
|
}
|
|
const plans = db.prepare(
|
|
'SELECT category_id, note FROM plans WHERE day = ?'
|
|
).all(day);
|
|
return {
|
|
day: td.day,
|
|
notes: td.notes,
|
|
entries: entries.map((e) => ({ ...e, values: valuesByEntry.get(e.id) || [] })),
|
|
plans,
|
|
};
|
|
}
|
|
|
|
export default async function entryRoutes(app) {
|
|
app.get('/api/day/:day', async (req, reply) => {
|
|
const { day } = req.params;
|
|
if (!ISO.test(day)) return reply.code(400).send({ error: 'Bad date' });
|
|
return dayPayload(day);
|
|
});
|
|
|
|
app.post('/api/entries', async (req, reply) => {
|
|
const { day, category_id, values, note } = req.body || {};
|
|
if (!ISO.test(day || '')) return reply.code(400).send({ error: 'Bad date' });
|
|
const cat = db.prepare('SELECT id FROM categories WHERE id = ?').get(Number(category_id));
|
|
if (!cat) return reply.code(400).send({ error: 'Unknown category' });
|
|
|
|
const newRecords = [];
|
|
const tx = db.transaction(() => {
|
|
ensureDay(day);
|
|
const { lastInsertRowid: entryId } = db.prepare(
|
|
'INSERT INTO entries (day, category_id, note) VALUES (?, ?, ?)'
|
|
).run(day, cat.id, String(note || ''));
|
|
writeValues(entryId, values, newRecords);
|
|
});
|
|
tx();
|
|
return { ...dayPayload(day), newRecords };
|
|
});
|
|
|
|
// Edit an existing entry: replace its values and note (and bump records).
|
|
app.put('/api/entries/:id', async (req, reply) => {
|
|
const id = Number(req.params.id);
|
|
const entry = db.prepare('SELECT id, day FROM entries WHERE id = ?').get(id);
|
|
if (!entry) return reply.code(404).send({ error: 'Not found' });
|
|
const { values, note } = req.body || {};
|
|
|
|
const newRecords = [];
|
|
const tx = db.transaction(() => {
|
|
if (note !== undefined) {
|
|
db.prepare('UPDATE entries SET note = ? WHERE id = ?').run(String(note || ''), id);
|
|
}
|
|
db.prepare('DELETE FROM entry_values WHERE entry_id = ?').run(id);
|
|
writeValues(id, values, newRecords);
|
|
});
|
|
tx();
|
|
return { ...dayPayload(entry.day), newRecords };
|
|
});
|
|
|
|
app.delete('/api/entries/:id', async (req, reply) => {
|
|
const id = Number(req.params.id);
|
|
const row = db.prepare('SELECT day FROM entries WHERE id = ?').get(id);
|
|
db.prepare('DELETE FROM entries WHERE id = ?').run(id);
|
|
return reply.send(row ? dayPayload(row.day) : { ok: true });
|
|
});
|
|
|
|
app.put('/api/day/:day/notes', async (req, reply) => {
|
|
const { day } = req.params;
|
|
if (!ISO.test(day)) return reply.code(400).send({ error: 'Bad date' });
|
|
const notes = String((req.body && req.body.notes) || '');
|
|
ensureDay(day);
|
|
db.prepare('UPDATE training_days SET notes = ? WHERE day = ?').run(notes, day);
|
|
return { ok: true, notes };
|
|
});
|
|
}
|