diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 1ed44fb..4d0b345 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -118,8 +118,11 @@ export default function Forecast() { 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 filters = [ - ...Object.entries(sliceObj).map(([col, val]) => [col, '==', val]), + ...Object.entries(sliceObj) + .filter(([col]) => dimNames.has(col)) + .map(([col, val]) => [col, '==', val]), ['pf_iter', '!=', 'reference'], ] const view = await tableRef.current.view({ filter: filters }) @@ -378,110 +381,123 @@ export default function Forecast() { const hasSlice = Object.keys(slice).length > 0 return ( -
+
{/* Source / version bar */} -
+
Source - setSourceId(e.target.value)} className="border border-gray-600 rounded px-2 py-1 text-sm bg-gray-800 text-gray-200"> {sources.map(s => )}
Version - setVersionId(e.target.value)} className="border border-gray-600 rounded px-2 py-1 text-sm bg-gray-800 text-gray-200" disabled={!versions.length}> {versions.length === 0 ? : versions.map(v => )} {selectedVersion && ( - + {selectedVersion.status} )}
- {msg && ( - - {msg.text} - - )}
- {/* Layout / depth bar */} -
- Layouts + {/* Toolbar bar */} +
- {layouts.map(l => ( -
applyLayout(l)} - className={`flex items-center gap-1 text-xs rounded px-2 py-0.5 cursor-pointer border transition-colors - ${activeLayoutId === l.id ? 'bg-blue-50 border-blue-300 text-blue-700' : 'bg-white border-gray-200 text-gray-600 hover:border-gray-400'}`}> - {l.name} - -
- ))} + {/* Layouts group */} +
+ Layout + {layouts.map(l => ( +
applyLayout(l)} + className={`flex items-center gap-1 rounded px-2 py-0.5 cursor-pointer border transition-colors + ${activeLayoutId === l.id ? 'bg-blue-900 border-blue-600 text-blue-300' : 'bg-gray-800 border-gray-600 text-gray-400 hover:border-gray-400 hover:text-gray-200'}`}> + {l.name} + +
+ ))} + {showSaveAs ? ( +
+ setSaveAsName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleSaveAs(); if (e.key === 'Escape') { setShowSaveAs(false); setSaveAsName('') } }} + placeholder="Layout name…" className="border border-gray-600 rounded px-2 py-0.5 w-32 bg-gray-800 text-gray-200 focus:outline-none focus:border-blue-500 placeholder:text-gray-600" /> + + +
+ ) : ( + <> + {activeLayoutId !== null && ( + + )} + + {activeLayoutId !== null && ( + + )} + + )} +
- {activeLayoutId !== null && !showSaveAs && ( - - )} +
- {showSaveAs ? ( -
- setSaveAsName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleSaveAs(); if (e.key === 'Escape') { setShowSaveAs(false); setSaveAsName('') } }} - placeholder="Layout name…" className="text-xs border border-gray-300 rounded px-2 py-0.5 w-32 focus:outline-none focus:border-blue-400" /> - - -
- ) : ( - - )} - - {activeLayoutId !== null && ( - - )} - - - - {/* Depth controls */} -
- depth + {/* Expand depth group */} +
+ Expand {[0, 1, 2, 3].map(d => ( ))} +
+ +
+ + {/* Data group */} +
+
+ + {msg && ( + + {msg.text} + + )} +
{/* History modal */} {showLog && ( -
setShowLog(false)}> -
e.stopPropagation()}> -
- Change History - +
setShowLog(false)}> +
e.stopPropagation()}> +
+ Change log +
{logLoading ? ( -
Loading…
+
Loading…
) : logEntries.length === 0 ? ( -
No log entries yet.
+
No log entries yet.
) : ( - + @@ -493,15 +509,15 @@ export default function Forecast() { {logEntries.map(entry => ( - - + + - - + @@ -527,7 +543,7 @@ export default function Forecast() { @@ -546,40 +562,40 @@ export default function Forecast() { {/* Perspective viewer */}
{loading && ( -
- Loading… +
+ Loading…
)}
{/* Drag handle */} -
+
{/* Operation panel */} -
-
-
Slice
+
+
+
Slice
{!hasSlice ? ( -
Click a pivot row to select a slice
+
Click a pivot row to select a slice
) : (
{Object.entries(slice).map(([k, v]) => ( -
- {k} = {v} +
+ {k} = {v}
))} - +
)}
{hasSlice && ( <> -
+
{['scale', 'recode', 'clone'].map(op => ( ))} @@ -588,10 +604,10 @@ export default function Forecast() {
{activeOp === 'scale' && <> {/* Mode toggle */} -
+
{[['target','= Target'],['delta','Δ Increment']].map(([m, label]) => ( ))} @@ -600,9 +616,9 @@ export default function Forecast() { {/* Value row */} {currentTotals?.valueCol && (
-
+
{currentTotals.valueCol} - {currentTotals.value?.toLocaleString(undefined, {maximumFractionDigits:2})} + {currentTotals.value?.toLocaleString(undefined, {maximumFractionDigits:2})}
setScaleValue(e.target.value)} @@ -614,9 +630,9 @@ export default function Forecast() { {/* Units row */} {currentTotals?.unitsCol && (
-
+
{currentTotals.unitsCol} - {currentTotals.units?.toLocaleString(undefined, {maximumFractionDigits:2})} + {currentTotals.units?.toLocaleString(undefined, {maximumFractionDigits:2})}
setScaleUnits(e.target.value)} @@ -636,7 +652,7 @@ export default function Forecast() { } {activeOp === 'recode' && <> -

New values for dimensions to replace. Leave blank to keep.

+

New values for dimensions to replace. Leave blank to keep.

{dimCols.map(c => ( setRecodeSet(s => ({ ...s, [c.cname]: e.target.value }))} @@ -648,7 +664,7 @@ export default function Forecast() { } {activeOp === 'clone' && <> -

Override dimensions on cloned rows. Leave blank to keep.

+

Override dimensions on cloned rows. Leave blank to keep.

{dimCols.map(c => ( setCloneSet(s => ({ ...s, [c.cname]: e.target.value }))} @@ -668,7 +684,7 @@ export default function Forecast() { ) } -const inp = 'border border-gray-200 rounded px-2 py-1 text-xs flex-1 bg-white min-w-0' +const inp = 'border border-gray-700 rounded px-2 py-1 text-xs flex-1 bg-gray-800 text-gray-200 placeholder:text-gray-600 min-w-0 focus:outline-none focus:border-blue-600' function fmtStamp(stamp) { return new Date(stamp).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) @@ -680,18 +696,18 @@ function fmtSlice(slice) { } const OP_BADGE = { - baseline: 'bg-gray-100 text-gray-600', - reference: 'bg-blue-50 text-blue-600', - scale: 'bg-green-50 text-green-700', - recode: 'bg-amber-50 text-amber-700', - clone: 'bg-purple-50 text-purple-700', + baseline: 'bg-gray-700 text-gray-300', + reference: 'bg-blue-900 text-blue-300', + scale: 'bg-green-900 text-green-400', + recode: 'bg-amber-900 text-amber-400', + clone: 'bg-purple-900 text-purple-400', } -function opBadge(op) { return OP_BADGE[op] || 'bg-gray-100 text-gray-500' } +function opBadge(op) { return OP_BADGE[op] || 'bg-gray-700 text-gray-400' } function Row({ label, children }) { return (
- {label} + {label} {children}
) @@ -699,7 +715,7 @@ function Row({ label, children }) { function Submit({ onClick, children }) { return ( - )
Time Op
{fmtStamp(entry.stamp)}
{fmtStamp(entry.stamp)} {entry.operation} {fmtSlice(entry.slice)} + {fmtSlice(entry.slice)} {editingNote?.id === entry.id ? (
- - + className="border border-blue-600 rounded px-1.5 py-0.5 text-xs flex-1 bg-gray-800 text-gray-200 focus:outline-none" /> + +
) : ( setEditingNote({ id: entry.id, text: entry.note || '' })} - className="cursor-text hover:bg-blue-50 rounded px-1 -mx-1 block truncate" + className="cursor-text hover:bg-gray-700 rounded px-1 -mx-1 block truncate" title={entry.note || 'Click to add note'}> - {entry.note || add note} + {entry.note || add note} )}