Fix slice handling: payload preview, date column support, subtotal filter
- Add PayloadPreview component showing the exact JSON that will be POSTed, live-updating as form fields change (value_incr shown as computed delta) - buildEffectiveSlice strips expression/system columns and converts Perspective ms-timestamps to ISO date strings for date-role columns - fetchCurrentTotals now includes date columns in Perspective view filter (passing ms number as Perspective expects) so subtotals respect the clicked date - Server buildWhere now receives filterCols (dimensions + date cols) so date values reach the SQL WHERE clause correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cf391286a2
commit
682968b820
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
)}
|
||||
|
||||
<Row label="Note"><input value={scaleNote} onChange={e => setScaleNote(e.target.value)} placeholder="optional" className={inp} /></Row>
|
||||
<PayloadPreview payload={buildPayload('scale')} />
|
||||
<Submit onClick={() => submitOp('scale')}>Apply Scale</Submit>
|
||||
</>}
|
||||
|
||||
@ -797,6 +848,7 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
|
||||
</Row>
|
||||
))}
|
||||
<Row label="Note"><input value={recodeNote} onChange={e => setRecodeNote(e.target.value)} placeholder="optional" className={inp} /></Row>
|
||||
<PayloadPreview payload={buildPayload('recode')} />
|
||||
<Submit onClick={() => submitOp('recode')}>Apply Recode</Submit>
|
||||
</>}
|
||||
|
||||
@ -810,6 +862,7 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou
|
||||
))}
|
||||
<Row label="Scale"><input type="number" step="any" value={cloneScale} onChange={e => setCloneScale(e.target.value)} className={inp} /></Row>
|
||||
<Row label="Note"><input value={cloneNote} onChange={e => setCloneNote(e.target.value)} placeholder="optional" className={inp} /></Row>
|
||||
<PayloadPreview payload={buildPayload('clone')} />
|
||||
<Submit onClick={() => submitOp('clone')}>Apply Clone</Submit>
|
||||
</>}
|
||||
</div>
|
||||
@ -868,3 +921,12 @@ function Submit({ onClick, children }) {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function PayloadPreview({ payload }) {
|
||||
if (!payload) return null
|
||||
return (
|
||||
<pre className="text-xs font-mono text-gray-400 bg-gray-50 border border-gray-100 rounded p-2 overflow-auto max-h-36 leading-relaxed">
|
||||
{JSON.stringify(payload, null, 2)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user