diff --git a/routes/operations.js b/routes/operations.js
index ca4cb6e..ca44ab9 100644
--- a/routes/operations.js
+++ b/routes/operations.js
@@ -31,6 +31,7 @@ module.exports = function(pool) {
);
const colMeta = colResult.rows;
const dimCols = colMeta.filter(c => c.role === 'dimension').map(c => c.cname);
+ const dateCols = colMeta.filter(c => c.role === 'date').map(c => c.cname);
const valueCol = colMeta.find(c => c.role === 'value')?.cname;
const unitsCol = colMeta.find(c => c.role === 'units')?.cname;
@@ -48,6 +49,8 @@ module.exports = function(pool) {
table: fcTable(version.tname, version.id),
colMeta,
dimCols,
+ dateCols,
+ filterCols: [...dimCols, ...dateCols],
valueCol,
unitsCol,
sql: sqlResult.rows[0].sql
@@ -303,7 +306,7 @@ module.exports = function(pool) {
const ctx = await getContext(parseInt(req.params.id), 'scale');
if (!guardOpen(ctx.version, res)) return;
- const whereClause = buildWhere(slice, ctx.dimCols);
+ const whereClause = buildWhere(slice, ctx.filterCols);
const excludeClause = buildExcludeClause(ctx.version.exclude_iters);
let absValueIncr = value_incr || 0;
@@ -361,7 +364,7 @@ module.exports = function(pool) {
const ctx = await getContext(parseInt(req.params.id), 'recode');
if (!guardOpen(ctx.version, res)) return;
- const whereClause = buildWhere(slice, ctx.dimCols);
+ const whereClause = buildWhere(slice, ctx.filterCols);
const excludeClause = buildExcludeClause(ctx.version.exclude_iters);
const setClause = buildSetClause(ctx.dimCols, set);
@@ -398,7 +401,7 @@ module.exports = function(pool) {
if (!guardOpen(ctx.version, res)) return;
const scaleFactor = (scale != null) ? parseFloat(scale) : 1.0;
- const whereClause = buildWhere(slice, ctx.dimCols);
+ const whereClause = buildWhere(slice, ctx.filterCols);
const excludeClause = buildExcludeClause(ctx.version.exclude_iters);
const setClause = buildSetClause(ctx.dimCols, set);
diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx
index 3fffda6..93192ca 100644
--- a/ui/src/views/Forecast.jsx
+++ b/ui/src/views/Forecast.jsx
@@ -113,11 +113,15 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
const unitsCol = colMetaRef.current.find(c => c.role === 'units')?.cname
if (!valueCol && !unitsCol) return
try {
- const dimNames = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
+ const dimNames = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
+ const dateNames = new Set(colMetaRef.current.filter(c => c.role === 'date').map(c => c.cname))
const filters = [
...Object.entries(sliceObj)
.filter(([col]) => dimNames.has(col))
.map(([col, val]) => [col, '==', val]),
+ ...Object.entries(sliceObj)
+ .filter(([col]) => dateNames.has(col))
+ .map(([col, val]) => [col, '==', Number(val)]),
['pf_iter', '!=', 'reference'],
]
const view = await tableRef.current.view({ filter: filters })
@@ -385,7 +389,9 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
async function submitOp(op) {
if (!Object.keys(slice).length) { flash('Select a slice first', 'error'); return }
- let body = { pf_user: 'admin', slice }
+ const effectiveSlice = buildEffectiveSlice(slice)
+ if (!Object.keys(effectiveSlice).length) { flash('No dimension or date columns in slice — check col_meta', 'error'); return }
+ let body = { pf_user: 'admin', slice: effectiveSlice }
if (op === 'scale') {
let vi = null, ui = null
@@ -430,6 +436,50 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
} catch (err) { flash(err.message, 'error') }
}
+ function buildEffectiveSlice(raw) {
+ const dimCols = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
+ const dateCols = new Set(colMetaRef.current.filter(c => c.role === 'date').map(c => c.cname))
+ const out = {}
+ for (const [k, v] of Object.entries(raw)) {
+ if (dimCols.has(k)) { out[k] = v; continue }
+ if (dateCols.has(k)) {
+ const ms = Number(v)
+ out[k] = isFinite(ms) ? new Date(ms).toISOString().slice(0, 10) : v
+ }
+ }
+ return out
+ }
+
+ function buildPayload(op) {
+ if (!Object.keys(slice).length) return null
+ const effectiveSlice = buildEffectiveSlice(slice)
+ let body = { pf_user: 'admin', slice: effectiveSlice }
+ if (op === 'scale') {
+ let vi = null, ui = null
+ if (scaleMode === 'target') {
+ const curValue = currentTotals?.total?.value
+ const curUnits = currentTotals?.total?.units
+ if (scalePrice !== '' && curUnits != null && curValue != null)
+ vi = (parseFloat(scalePrice) * curUnits) - curValue
+ if (scaleValue !== '' && curValue != null)
+ vi = parseFloat(scaleValue) - curValue
+ if (scaleUnits !== '' && curUnits != null)
+ ui = parseFloat(scaleUnits) - curUnits
+ } else {
+ if (scaleValue !== '') vi = parseFloat(scaleValue)
+ if (scaleUnits !== '') ui = parseFloat(scaleUnits)
+ }
+ body = { ...body, note: scaleNote || undefined, value_incr: vi, units_incr: ui, pct: scaleMode === 'delta' && scalePct }
+ } else if (op === 'recode') {
+ const set = Object.fromEntries(Object.entries(recodeSet).filter(([, v]) => v.trim()))
+ body = { ...body, note: recodeNote || undefined, set }
+ } else if (op === 'clone') {
+ const set = Object.fromEntries(Object.entries(cloneSet).filter(([, v]) => v.trim()))
+ body = { ...body, note: cloneNote || undefined, set, scale: parseFloat(cloneScale) || 1 }
+ }
+ return body
+ }
+
function flash(text, type = 'ok') {
setMsg({ text, type })
setTimeout(() => setMsg(null), 3000)
@@ -785,6 +835,7 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
)}
+ {JSON.stringify(payload, null, 2)}
+
+ )
+}