diff --git a/ui/src/components/BridgeView.jsx b/ui/src/components/BridgeView.jsx index c4781ae..f61b71d 100644 --- a/ui/src/components/BridgeView.jsx +++ b/ui/src/components/BridgeView.jsx @@ -106,7 +106,9 @@ export function buildSteps(rows, { if (isLoad) { loads.value += v; loads.units += u; loads.rows += 1; continue } const meta = logMeta[r.pf_logid] || {} - const tag = (meta.tag || '').trim() + // label first, the same precedence pf_segment uses, so the bridge and the + // pivot call a step by the same name + const tag = (meta.label || meta.tag || '').trim() const label = tag || (meta.note || '').trim() || `${(meta.operation || r.pf_iter || 'adj')}${r.pf_logid != null ? ` #${r.pf_logid}` : ''}` const key = tag ? `tag:${tag}` : `log:${r.pf_logid}` diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx index df16b0b..fcdb223 100644 --- a/ui/src/components/OperationPanel.jsx +++ b/ui/src/components/OperationPanel.jsx @@ -287,9 +287,16 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se const loose = [] const byTag = new Map() for (const e of entries) { - if (e.key === 'baseline') { baseline.push({ ...e, label: 'Baseline', kind: 'baseline' }); continue } const meta = logMeta[e.logid] || {} - const tag = (meta.tag || '').trim() + // Named like every other line: the label the pivot shows, then the older + // fallbacks. "Baseline" was hardcoded, so a segment called 03 - New Orders + // everywhere else read as "Baseline" here alone. + if (e.key === 'baseline') { + const name = (meta.label || meta.tag || meta.note || '').trim() + baseline.push({ ...e, label: name || 'Baseline', kind: 'baseline' }) + continue + } + const tag = (meta.label || meta.tag || '').trim() if (tag) { const g = byTag.get(tag) || { key: `tag:${tag}`, label: tag, kind: 'tag', value: 0, units: 0, count: 0, first: e.logid } @@ -302,7 +309,8 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se const op = meta.operation || e.iter || 'adjustment' loose.push({ ...e, kind: 'entry', count: 1, - label: (meta.note || '').trim() || `${op.charAt(0).toUpperCase()}${op.slice(1)} #${e.logid}`, + label: (meta.label || meta.note || '').trim() + || `${op.charAt(0).toUpperCase()}${op.slice(1)} #${e.logid}`, }) } } @@ -327,6 +335,12 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se // otherwise prints a row of zeros and leaves the reason to be worked out. const nothingToAdjust = hasExcl && !total.value && !total.units + // One line per immovable segment. Falls back to the combined figure for a + // selection whose rows carry no segment name. + const exclLines = (currentTotals?.excluded?.bySegment?.length + ? currentTotals.excluded.bySegment + : (hasExcl ? [{ name: exclName, ...excl }] : [])) + // the basis decides which line the editable rows are measured from const basisOf = (key) => { if (!onTotal) return key === 'price' ? curPrice : total[key] @@ -468,21 +482,21 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se {/* Rows the pivot shows but operations cannot write. Listed so the panel's figures reconcile with what the grid displays. */} - {hasExcl && ( - - - {exclName} - + {exclLines.map(seg => ( + + + {seg.name} + final {measures.map(m => ( - {m.key === 'price' ? fmtNum(priceOf(excl), m.dp) : fmtNum(excl[m.key], m.dp)} + {m.key === 'price' ? fmtNum(priceOf(seg), m.dp) : fmtNum(seg[m.key], m.dp)} ))} - )} + ))} {hasExcl && ( diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index d0f2d9b..3618c8f 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -497,7 +497,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // rows the pivot shows but operations cannot write (usually 'reference'). // Kept separate rather than filtered away: the grid total includes them, // so the panel has to account for them or the two disagree. - const excluded = { value: 0, units: 0, rows: 0, names: new Set() } + // Per segment, not one lump: YTD Sales and Open Orders are different + // things, and a single "final" line hides which part of the number is + // which. + const excluded = { value: 0, units: 0, rows: 0, names: new Set(), bySegment: new Map() } for (const r of rows) { const k = r.pf_iter || '?' const val = valueCol ? (parseFloat(r[valueCol]) || 0) : 0 @@ -510,7 +513,13 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // Name them by what they are, not by the iter band that happens to // exclude them: "02 - Prior Year" means something to a forecaster, // "reference" is the mechanism. - if (r.pf_segment) excluded.names.add(String(r.pf_segment)) + const name = String(r.pf_segment || 'excluded') + excluded.names.add(name) + const seg = excluded.bySegment.get(name) || { name, value: 0, units: 0, rows: 0 } + seg.value += val + seg.units += uni + seg.rows += 1 + excluded.bySegment.set(name, seg) continue } @@ -583,11 +592,24 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio units: acc.units + (ps.excluded?.units || 0), rows: acc.rows + (ps.excluded?.rows || 0), names: new Set([...acc.names, ...(ps.excluded?.names || [])]), - }), { value: 0, units: 0, rows: 0, names: new Set() }) + bySegment: (() => { + const m = acc.bySegment + for (const seg of (ps.excluded?.bySegment?.values?.() || [])) { + const t = m.get(seg.name) || { name: seg.name, value: 0, units: 0, rows: 0 } + t.value += seg.value; t.units += seg.units; t.rows += seg.rows + m.set(seg.name, t) + } + return m + })(), + }), { value: 0, units: 0, rows: 0, names: new Set(), bySegment: new Map() }) setCurrentTotals({ byIter, byEntry, total, valueCol, unitsCol, perSlice, - excluded: { ...excluded, names: [...excluded.names].sort() }, + excluded: { + ...excluded, + names: [...excluded.names].sort(), + bySegment: [...excluded.bySegment.values()].sort((a, b) => a.name.localeCompare(b.name)), + }, excludedIters: [...excludeIters], }) } catch { @@ -603,8 +625,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const entries = await fetch(`/api/versions/${vid}/log`).then(r => r.json()) const map = {} for (const e of entries) map[e.id] = { + label: e.label || null, tag: e.tag || null, note: e.note || null, operation: e.operation, - bucket: e.bucket || null, seq: e.seq ?? null, + bucket: e.bucket || null, } setLogMeta(map) } catch { setLogMeta({}) } @@ -1486,7 +1509,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio .map(([id, m]) => ({ id: Number(id), operation: m.operation, - label: (m.tag || m.note || '').trim(), + label: (m.label || m.tag || m.note || '').trim(), })) .sort((a, b) => a.id - b.id)