diff --git a/lib/sql_generator.js b/lib/sql_generator.js index 6f45032..01c485c 100644 --- a/lib/sql_generator.js +++ b/lib/sql_generator.js @@ -69,6 +69,9 @@ SELECT count(*) AS rows_affected FROM ins`.trim(); } function buildReference() { + const referenceSelect = dataCols.map(c => + c === dateCol ? `(${q(c)} + '{{date_offset}}'::interval)::date` : q(c) + ).join(', '); return ` WITH ilog AS ( @@ -78,7 +81,7 @@ ilog AS ( ) ,ins AS ( INSERT INTO {{fc_table}} (${insertCols}) - SELECT ${selectData}, 'reference', (SELECT id FROM ilog), '{{pf_user}}', now() + SELECT ${referenceSelect}, 'reference', (SELECT id FROM ilog), '{{pf_user}}', now() FROM ${srcTable} WHERE {{filter_clause}} RETURNING * diff --git a/routes/operations.js b/routes/operations.js index b2b01aa..ca4cb6e 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -192,28 +192,18 @@ module.exports = function(pool) { const paramsJson = JSON.stringify({ where_clause: filterClause, - ...(oldLog.operation === 'baseline' ? { date_offset: dateOffset } : {}), + date_offset: dateOffset, ...(raw_where ? { raw_where } : (filters ? { filters } : {})) }); - const tokens = oldLog.operation === 'baseline' - ? { - fc_table: ctx.table, - version_id: ctx.version.id, - pf_user: esc(pf_user || ''), - note: esc(note || ''), - params: esc(paramsJson), - filter_clause: filterClause, - date_offset: esc(dateOffset) - } - : { - fc_table: ctx.table, - version_id: ctx.version.id, - pf_user: esc(pf_user || ''), - note: esc(note || ''), - params: esc(paramsJson), - filter_clause: filterClause - }; - const sql = applyTokens(ctx.sql, tokens); + const sql = applyTokens(ctx.sql, { + fc_table: ctx.table, + version_id: ctx.version.id, + pf_user: esc(pf_user || ''), + note: esc(note || ''), + params: esc(paramsJson), + filter_clause: filterClause, + date_offset: esc(dateOffset) + }); await client.query('BEGIN'); const delRows = await client.query( @@ -274,13 +264,15 @@ module.exports = function(pool) { // load reference rows from source table (additive — does not clear prior reference rows) router.post('/versions/:id/reference', async (req, res) => { - const { where_clause, pf_user, note, filters, raw_where } = req.body; + const { where_clause, date_offset, pf_user, note, filters, raw_where } = req.body; + const dateOffset = date_offset || '0 days'; const filterClause = (raw_where || where_clause || '').trim() || 'TRUE'; try { const ctx = await getContext(parseInt(req.params.id), 'reference'); if (!guardOpen(ctx.version, res)) return; const paramsJson = JSON.stringify({ where_clause: filterClause, + date_offset: dateOffset, ...(raw_where ? { raw_where } : (filters ? { filters } : {})) }); const sql = applyTokens(ctx.sql, { @@ -289,7 +281,8 @@ module.exports = function(pool) { pf_user: esc(pf_user || ''), note: esc(note || ''), params: esc(paramsJson), - filter_clause: filterClause + filter_clause: filterClause, + date_offset: esc(dateOffset) }); const result = await runSQL(sql); diff --git a/todo.md b/todo.md index db3085b..42d3579 100644 --- a/todo.md +++ b/todo.md @@ -1,4 +1,7 @@ -- [ ] should be able to edit and revise forecase segments that constitute baseline or reference. if you edit, maybe a warning that your forecast values wont mean a lot, and have an option to delete them. +- [ ] when you enter the forecast, be able to enter in a context so you dont have to open the whole thing (should show in status bar and be a filter for SQL and spi calls) + + +- [x] should be able to edit and revise forecase segments that constitute baseline or reference. if you edit, maybe a warning that your forecast values wont mean a lot, and have an option to delete them. Notes: A baseline/reference segment is a `pf.log` row plus the forecast rows it produced (joined by pf_logid). Editing has the @@ -20,7 +23,7 @@ Implementation order: API + cascade detection first (compare pf.log.stamp ordering); UI second. -- [ ] be able to copy an existing forecast and it's segments to adjust some parameters without having to start from scrath. +- [~] be able to copy an existing forecast and it's segments to adjust some parameters without having to start from scrath. Notes: A version is the unit of copy. Need a `POST /versions/:id/copy` endpoint that creates a new pf.version row with the same source/ @@ -43,7 +46,7 @@ - Should the copy track its origin? A `parent_version_id` column on pf.version makes "show me variants of FY2026 Plan" easy. -- [ ] need the list of filters to have an and/or specification +- [x] need the list of filters to have an and/or specification Notes: Spec already covers this in `pf_spec.md:245` — `filters` is an array of groups; conditions within a group are AND-ed, groups @@ -58,7 +61,7 @@ already has a single-group filter builder; extend it to wrap the current rows in a group container and allow adding more groups. -- [ ] the filters should have the option to just write the WHERE clause SQL +- [x] the filters should have the option to just write the WHERE clause SQL Notes: Spec covers this too (`pf_spec.md:251`, `:454`) as the `raw_where` admin-only escape hatch. The current baseline endpoint diff --git a/ui/src/views/Baseline.jsx b/ui/src/views/Baseline.jsx index bfb19fc..f446b3f 100644 --- a/ui/src/views/Baseline.jsx +++ b/ui/src/views/Baseline.jsx @@ -144,15 +144,13 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio const clause = useRaw ? rawSql.trim() : buildFilterClause(filters) if (!clause) { flash(useRaw ? 'Enter a WHERE clause' : 'Add at least one filter', 'error'); return } const isRef = segType === 'reference' - const offsetStr = isRef - ? '0 days' - : ([offsetYr > 0 ? `${offsetYr} year` : '', offsetMo > 0 ? `${offsetMo} month` : ''].filter(Boolean).join(' ') || '0 days') + const offsetStr = [offsetYr > 0 ? `${offsetYr} year` : '', offsetMo > 0 ? `${offsetMo} month` : ''].filter(Boolean).join(' ') || '0 days' const endpoint = isRef ? 'reference' : 'baseline' const body = { where_clause: clause, pf_user: 'admin', note: description || segNote, - ...(isRef ? {} : { date_offset: offsetStr }), + date_offset: offsetStr, ...(useRaw ? { raw_where: clause } : { filters }), } setSubmitting(true) @@ -189,13 +187,9 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio setSegType(entry.operation) setSegNote(entry.note || '') setDescription('') - if (entry.operation === 'baseline') { - const off = parseOffset(params.date_offset) - setOffsetYr(off.yr) - setOffsetMo(off.mo) - } else { - setOffsetYr(0); setOffsetMo(0) - } + const off = parseOffset(params.date_offset) + setOffsetYr(off.yr) + setOffsetMo(off.mo) const groups = normalizeFilters(params.filters) if (groups) { setUseRaw(false) @@ -374,7 +368,7 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio setExpandedId(isOpen ? null : entry.id)} - className={`border-t border-gray-50 cursor-pointer hover:bg-gray-50 ${isOpen ? 'bg-blue-50' : ''}`} + className={`border-t border-gray-50 cursor-pointer hover:bg-gray-50 ${editingLogId === entry.id ? 'bg-amber-50 ring-1 ring-amber-300 ring-inset' : isOpen ? 'bg-blue-50' : ''}`} > {isOpen ? '▾' : '▸'} {log.length - i} @@ -417,14 +411,18 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio {/* Add / Edit Segment */} {(showAddForm || editingLogId) && ( -
-
+
+
{(() => { if (!editingLogId) return 'Add Segment' - const entry = log.find(e => e.id === editingLogId) - if (!entry) return 'Edit Segment' - const label = entry.operation === 'reference' ? 'reference' : 'baseline' - return entry.note ? `Edit ${label} — ${entry.note}` : `Edit ${label} segment` + const idx = log.findIndex(e => e.id === editingLogId) + if (idx < 0) return 'Edit Segment' + const entry = log[idx] + const segNum = log.length - idx + const label = entry.operation === 'reference' ? 'reference' : 'baseline' + return entry.note + ? `Edit segment #${segNum} — ${label} — ${entry.note}` + : `Edit segment #${segNum} — ${label}` })()} ))}
- {segType === 'reference' && ( - dates land verbatim — no offset applied - )}
{/* Description (edit only) */} @@ -657,18 +652,16 @@ function SegmentForm({ )}
- {/* Date offset — baseline only */} - {segType === 'baseline' && ( -
- -
- setOffsetYr(parseInt(e.target.value) || 0)} className={`${baseInp} text-sm w-16 text-center`} /> - yr - setOffsetMo(parseInt(e.target.value) || 0)} className={`${baseInp} text-sm w-16 text-center`} /> - mo -
+ {/* Date offset */} +
+ +
+ setOffsetYr(parseInt(e.target.value) || 0)} className={`${baseInp} text-sm w-16 text-center`} /> + yr + setOffsetMo(parseInt(e.target.value) || 0)} className={`${baseInp} text-sm w-16 text-center`} /> + mo
- )} +
{/* Timeline */} {dateRange && ( @@ -677,8 +670,8 @@ function SegmentForm({