diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx index b0f8c56..566ff1a 100644 --- a/ui/src/components/OperationPanel.jsx +++ b/ui/src/components/OperationPanel.jsx @@ -61,6 +61,34 @@ function Button({ onClick, active, children, title }) { ) } +// A label and its control, on a grid. Every row in the panel uses the same label +// width, so the controls line up down the column instead of each row starting +// wherever its label happens to end -- which was the whole problem with +// "copy rows from" sitting above "scale cloned rows by" above "tag". +const LABEL_W = 'w-24' + +function Field({ label, children, hint }) { + return ( +
+ {label} +
+
{children}
+ {hint &&

{hint}

} +
+
+ ) +} + +// Same 10px uppercase as the ledger table headers, so the groupings read as part +// of the same family rather than as a second style. +function SectionLabel({ children }) { + return ( +
+ {children} +
+ ) +} + function Segmented({ options, value, onChange }) { return (
@@ -639,9 +667,16 @@ function DimForm({ dimCols, setObj, setSet, slices, lookupDerivedCols, dimMember // Recode and clone change dimensions rather than amounts, but you still want to // see how much is on the move — and for clone, what it becomes after scaling. -function MovingTotal({ currentTotals, verb, factor }) { - const t = currentTotals?.total - if (!t) return null +// includeExcluded: clone reads reference rows too, so its preview has to count +// them. Reporting the adjustable total alone said "Copying 0.00" for a selection +// made entirely of prior year or plan -- the exact case clone exists for. +function MovingTotal({ currentTotals, verb, factor, includeExcluded }) { + const adj = currentTotals?.total + if (!adj) return null + const ex = currentTotals?.excluded + const t = includeExcluded && ex + ? { value: (adj.value || 0) + (ex.value || 0), units: (adj.units || 0) + (ex.units || 0) } + : adj const { valueCol, unitsCol } = currentTotals const scaled = factor != null && factor !== 1 return ( @@ -725,6 +760,7 @@ export default function OperationPanel({ const horizontal = dock === 'bottom' const note = activeOp === 'scale' ? scaleNote : activeOp === 'recode' ? recodeNote : cloneNote + const shifting = !!cloneOffset && cloneOffset.trim() !== '' && cloneOffset.trim() !== '0 days' const setNote = activeOp === 'scale' ? setScaleNote : activeOp === 'recode' ? setRecodeNote : setCloneNote const OP_LABEL = { scale: 'Apply Scale', recode: 'Apply Recode', clone: 'Apply Clone' } @@ -779,24 +815,32 @@ export default function OperationPanel({ {activeOp === 'recode' && ( } /> + extra={ +
+ +
+ } /> )} {activeOp === 'clone' && ( - {/* The selection is the SOURCE, not the destination: pick - the cells you want to copy -- last December, say -- and - the shift below is what lands them in the target period. - Naming a segment narrows that selection to one entry, - including the reference ones operations are normally kept - away from, which is the point: a period with no baseline - borrows its shape from prior year or plan. */} -
- copy rows from +
+ source + + {/* The selection is the SOURCE, not the destination: pick the + cells you want to copy -- last December, say -- and the + shift is what lands them in the target period. Naming a + segment narrows that selection to one entry, including the + reference ones operations are normally kept away from, + which is the point: a period with no baseline borrows its + shape from prior year or plan. */} + -
+ - {/* Always shown. This was gated on picking a segment, from - when naming one was the only way to reach reference rows - -- once clone could read them directly, the offset became - unreachable in the ordinary case. */} -
- shift dates by - setCloneOffset(e.target.value)} - placeholder="0 days" className={`${INPUT} w-28`} /> - - -
- {cloneOffset.trim() && cloneOffset.trim() !== '0 days' && ( -

- The cells you have selected are what gets copied. Shifting - moves every date column, and the season and month - dimensions are re-derived from the calendar to match — so - select the period you are copying from, not the - one you are filling. -

- )} + changes -
- scale cloned rows by + + setCloneOffset(e.target.value)} + placeholder="0 days" className={`${TEXT} w-28`} /> + + + + + setCloneScale(e.target.value)} className={INPUT} /> -
- {!cloneFrom && ( + onChange={e => setCloneScale(e.target.value)} + className={`${INPUT} w-28`} /> + × + + + {/* Sits under the controls it reflects rather than floating + after them, and counts the reference rows clone can now + read -- it used to report the adjustable total, which is + zero when the selection is entirely prior year or plan. */} +
- )} + factor={parseFloat(cloneScale) || 1} + includeExcluded /> +
} /> )} @@ -852,11 +894,12 @@ export default function OperationPanel({ {hasSlice && (
+ label this change + {/* Tag first: it is the field that gives an adjustment meaning later, in the ledger and in the bridge. Completes from initiatives already used on this source; free text is still accepted. */} -
- tag + setOpTag(e.target.value)} list="pf-tag-options" placeholder="initiative, e.g. reduce_spend" @@ -865,27 +908,35 @@ export default function OperationPanel({ )} -
+ + + {/* Under the tag field and inside the same column, so they read as + values for it rather than as a row of unexplained buttons. */} {knownTags.length > 0 && ( -
- {knownTags.slice(0, 6).map(t => ( - - ))} -
+ +
+ {knownTags.slice(0, 6).map(t => ( + + ))} +
+
)} -
- note - setNote(e.target.value)} placeholder="optional" className={TEXT} /> + + setNote(e.target.value)} + placeholder="optional" className={`${TEXT} w-48`} /> + + +
+ submitOp(activeOp)}>{OP_LABEL[activeOp]}
- submitOp(activeOp)}>{OP_LABEL[activeOp]}