diff --git a/routes/sources.js b/routes/sources.js index 4347774..5b2352e 100644 --- a/routes/sources.js +++ b/routes/sources.js @@ -224,6 +224,41 @@ module.exports = function(pool) { } }); + // given a key column value, look up sibling dim_group column values from source + // returns { sibling_col: value, ... } if exactly one match, null if none or ambiguous + router.get('/sources/:id/lookup', async (req, res) => { + const { col, value } = req.query; + if (!col || value == null || value === '') return res.json(null); + try { + const [srcResult, metaResult] = await Promise.all([ + pool.query(`SELECT schema, tname FROM pf.source WHERE id = $1`, [req.params.id]), + pool.query(`SELECT * FROM pf.col_meta WHERE source_id = $1 ORDER BY opos`, [req.params.id]) + ]); + if (srcResult.rows.length === 0) return res.status(404).json({ error: 'Source not found' }); + + const keyCol = metaResult.rows.find(c => c.cname === col && c.is_key && c.dim_group); + if (!keyCol) return res.json(null); + + const siblings = metaResult.rows.filter(c => + c.dim_group === keyCol.dim_group && c.cname !== col + ); + if (!siblings.length) return res.json(null); + + const { schema, tname } = srcResult.rows[0]; + const sibCols = siblings.map(c => `"${c.cname}"`).join(', '); + const result = await pool.query( + `SELECT DISTINCT ${sibCols} FROM "${schema}"."${tname}" WHERE "${col}" = $1 LIMIT 2`, + [value] + ); + + if (result.rows.length !== 1) return res.json(null); + res.json(result.rows[0]); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } + }); + // set or clear the default Perspective layout for a source. // Body: a Perspective view config (group_by, split_by, columns, plugin_config, …). // Pass null or {} to clear. diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 93192ca..94e85fd 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -436,6 +436,21 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou } catch (err) { flash(err.message, 'error') } } + async function lookupDerivedCols(col, value, setter) { + if (!sourceId || !value.trim()) return + const res = await fetch(`/api/sources/${sourceId}/lookup?col=${encodeURIComponent(col)}&value=${encodeURIComponent(value)}`) + if (!res.ok) return + const derived = await res.json() + if (!derived) return + setter(prev => { + const next = { ...prev } + for (const [k, v] of Object.entries(derived)) { + if (!prev[k] || prev[k] === '') next[k] = String(v ?? '') + } + return next + }) + } + 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)) @@ -842,9 +857,15 @@ export default function Forecast({ sources = [], sourceId, versionId, refreshSou {activeOp === 'recode' && <>
New values for dimensions to replace. Leave blank to keep.
{dimCols.map(c => ( -Override dimensions on cloned rows. Leave blank to keep.
{dimCols.map(c => ( -