diff --git a/routes/log.js b/routes/log.js index 85f84f8..d14d0f1 100644 --- a/routes/log.js +++ b/routes/log.js @@ -4,98 +4,100 @@ const { fcTable } = require('../lib/utils'); module.exports = function(pool) { const router = express.Router(); - // list all log entries for a version, newest first, with row counts from fc_table + // list log entries for a version, newest first, with row counts and value/units totals router.get('/versions/:id/log', async (req, res) => { + const versionId = parseInt(req.params.id); try { - const verResult = await pool.query(` - SELECT v.*, s.tname - FROM pf.version v - JOIN pf.source s ON s.id = v.source_id - WHERE v.id = $1 - `, [req.params.id]); - if (verResult.rows.length === 0) return res.status(404).json({ error: 'Version not found' }); - - const { tname, id: version_id } = verResult.rows[0]; - const table = fcTable(tname, version_id); - - const result = await pool.query( - `SELECT l.*, - counts.row_count - FROM pf.log l - LEFT JOIN ( - SELECT pf_logid, count(*)::int AS row_count - FROM ${table} - GROUP BY pf_logid - ) counts ON counts.pf_logid = l.id - WHERE l.version_id = $1 - ORDER BY l.stamp DESC`, - [req.params.id] + const verResult = await pool.query( + `SELECT v.*, s.tname, s.id AS source_id FROM pf.version v JOIN pf.source s ON s.id = v.source_id WHERE v.id = $1`, + [versionId] ); + if (!verResult.rows.length) return res.status(404).json({ error: 'Version not found' }); + const { tname, source_id } = verResult.rows[0]; + const table = fcTable(tname, versionId); + + const colMeta = await pool.query( + `SELECT cname, role FROM pf.col_meta WHERE source_id = $1 AND role IN ('value', 'units')`, + [source_id] + ); + const valueCol = colMeta.rows.find(c => c.role === 'value')?.cname; + const unitsCol = colMeta.rows.find(c => c.role === 'units')?.cname; + + const aggCols = [ + `count(f.pf_id)::int AS row_count`, + valueCol ? `sum(f."${valueCol}")::float8 AS value_total` : `NULL::float8 AS value_total`, + unitsCol ? `sum(f."${unitsCol}")::float8 AS units_total` : `NULL::float8 AS units_total` + ].join(', '); + + const result = await pool.query(` + SELECT l.*, ${aggCols}, + $2::text AS value_col, + $3::text AS units_col + FROM pf.log l + LEFT JOIN ${table} f ON f.pf_logid = l.id + WHERE l.version_id = $1 + GROUP BY l.id + ORDER BY l.id DESC + `, [versionId, valueCol || null, unitsCol || null]); res.json(result.rows); } catch (err) { console.error(err); - res.status(500).json({ error: err.message }); + res.status(err.status || 500).json({ error: err.message }); } }); - // update note on a log entry + // undo a log entry — delete all fc rows with this logid, then delete the log entry + router.delete('/log/:logid', async (req, res) => { + const logId = parseInt(req.params.logid); + try { + const logResult = await pool.query(` + SELECT l.*, v.status, s.tname, v.id AS version_id + FROM pf.log l + JOIN pf.version v ON v.id = l.version_id + JOIN pf.source s ON s.id = v.source_id + WHERE l.id = $1 + `, [logId]); + if (!logResult.rows.length) return res.status(404).json({ error: 'Log entry not found' }); + const log = logResult.rows[0]; + if (log.status === 'closed') return res.status(403).json({ error: 'Version is closed' }); + const table = fcTable(log.tname, log.version_id); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + const deleted = await client.query( + `DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, [logId] + ); + await client.query('DELETE FROM pf.log WHERE id = $1', [logId]); + await client.query('COMMIT'); + res.json({ + rows_deleted: deleted.rowCount, + pf_ids: deleted.rows.map(r => r.pf_id) + }); + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } + } catch (err) { + console.error(err); + res.status(err.status || 500).json({ error: err.message }); + } + }); + + // update the note on a log entry router.patch('/log/:logid', async (req, res) => { + const logId = parseInt(req.params.logid); const { note } = req.body; try { const result = await pool.query( - `UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`, - [note ?? null, parseInt(req.params.logid)] + `UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`, [note ?? null, logId] ); - if (result.rows.length === 0) return res.status(404).json({ error: 'Log entry not found' }); + if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' }); res.json(result.rows[0]); } catch (err) { console.error(err); - res.status(500).json({ error: err.message }); - } - }); - - // undo an operation — deletes all forecast rows with this logid, then the log entry - // two separate queries in a transaction to avoid FK ordering issues - router.delete('/log/:logid', async (req, res) => { - const logid = parseInt(req.params.logid); - const client = await pool.connect(); - try { - // look up the log entry to find the version and fc_table name - const logResult = await client.query(` - SELECT l.*, v.id AS version_id, s.tname - FROM pf.log l - JOIN pf.version v ON v.id = l.version_id - JOIN pf.source s ON s.id = v.source_id - WHERE l.id = $1 - `, [logid]); - - if (logResult.rows.length === 0) { - return res.status(404).json({ error: 'Log entry not found' }); - } - - const { tname, version_id } = logResult.rows[0]; - const table = fcTable(tname, version_id); - - await client.query('BEGIN'); - // delete forecast rows first (logid has no FK constraint — managed by app) - const del = await client.query( - `DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, - [logid] - ); - await client.query(`DELETE FROM pf.log WHERE id = $1`, [logid]); - await client.query('COMMIT'); - - res.json({ - message: 'Operation undone', - rows_deleted: del.rowCount, - pf_ids: del.rows.map(r => r.pf_id) - }); - } catch (err) { - await client.query('ROLLBACK'); - console.error(err); - res.status(500).json({ error: err.message }); - } finally { - client.release(); + res.status(err.status || 500).json({ error: err.message }); } }); diff --git a/routes/operations.js b/routes/operations.js index 1fa5345..b7da282 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -430,102 +430,8 @@ module.exports = function(pool) { } }); - // list log entries for a version, newest first, with row counts - router.get('/versions/:id/log', async (req, res) => { - const versionId = parseInt(req.params.id); - try { - const verResult = await pool.query( - `SELECT v.*, s.tname, s.id AS source_id FROM pf.version v JOIN pf.source s ON s.id = v.source_id WHERE v.id = $1`, - [versionId] - ); - if (!verResult.rows.length) return res.status(404).json({ error: 'Version not found' }); - const { tname, source_id } = verResult.rows[0]; - const table = fcTable(tname, versionId); - - const colMeta = await pool.query( - `SELECT cname, role FROM pf.col_meta WHERE source_id = $1 AND role IN ('value', 'units')`, - [source_id] - ); - const valueCol = colMeta.rows.find(c => c.role === 'value')?.cname; - const unitsCol = colMeta.rows.find(c => c.role === 'units')?.cname; - - const aggCols = [ - `count(f.pf_id)::int AS row_count`, - valueCol ? `sum(f."${valueCol}")::float8 AS value_total` : `NULL::float8 AS value_total`, - unitsCol ? `sum(f."${unitsCol}")::float8 AS units_total` : `NULL::float8 AS units_total` - ].join(', '); - - const result = await pool.query(` - SELECT l.*, ${aggCols}, - $2::text AS value_col, - $3::text AS units_col - FROM pf.log l - LEFT JOIN ${table} f ON f.pf_logid = l.id - WHERE l.version_id = $1 - GROUP BY l.id - ORDER BY l.id DESC - `, [versionId, valueCol || null, unitsCol || null]); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(err.status || 500).json({ error: err.message }); - } - }); - - // undo a log entry — delete all fc rows with this logid, then delete the log entry - router.delete('/log/:logid', async (req, res) => { - const logId = parseInt(req.params.logid); - try { - const logResult = await pool.query(` - SELECT l.*, v.status, s.tname, v.id AS version_id - FROM pf.log l - JOIN pf.version v ON v.id = l.version_id - JOIN pf.source s ON s.id = v.source_id - WHERE l.id = $1 - `, [logId]); - if (!logResult.rows.length) return res.status(404).json({ error: 'Log entry not found' }); - const log = logResult.rows[0]; - if (log.status === 'closed') return res.status(403).json({ error: 'Version is closed' }); - const table = fcTable(log.tname, log.version_id); - const client = await pool.connect(); - try { - await client.query('BEGIN'); - const deleted = await client.query( - `DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, [logId] - ); - await client.query('DELETE FROM pf.log WHERE id = $1', [logId]); - await client.query('COMMIT'); - res.json({ - rows_deleted: deleted.rowCount, - pf_ids: deleted.rows.map(r => r.pf_id) - }); - } catch (err) { - await client.query('ROLLBACK'); - throw err; - } finally { - client.release(); - } - } catch (err) { - console.error(err); - res.status(err.status || 500).json({ error: err.message }); - } - }); - - // update the note on a log entry - router.patch('/log/:logid', async (req, res) => { - const logId = parseInt(req.params.logid); - const { note } = req.body; - try { - const result = await pool.query( - `UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`, [note, logId] - ); - if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' }); - res.json(result.rows[0]); - } catch (err) { - console.error(err); - res.status(err.status || 500).json({ error: err.message }); - } - }); + // log routes (GET /versions/:id/log, DELETE /log/:logid, PATCH /log/:logid) + // live in routes/log.js — see that file. return router; };