const express = require('express'); const { fcTable } = require('../lib/utils'); module.exports = function(pool) { const router = express.Router(); // 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, 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 and/or tag on a log entry. Both are annotations — they never // affect the forecast rows — so they stay editable after the fact, including on // a closed version, where relabelling history is still legitimate. router.patch('/log/:logid', async (req, res) => { const logId = parseInt(req.params.logid); const { note, tag } = req.body; if (note === undefined && tag === undefined) { return res.status(400).json({ error: 'Nothing to update — send note and/or tag' }); } try { // COALESCE on the flag, not the value: an explicit null or '' must be // able to clear a field, which COALESCE on the value alone would ignore const result = await pool.query( `UPDATE pf.log SET note = CASE WHEN $2::bool THEN $3::text ELSE note END, tag = CASE WHEN $4::bool THEN $5::text ELSE tag END WHERE id = $1 RETURNING *`, [ logId, note !== undefined, note === undefined ? null : (String(note).trim() || null), tag !== undefined, tag === undefined ? null : (String(tag).trim() || null), ] ); 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 }); } }); return router; };