Name the ledger's lines the way everything else names them
The walk read from tag and note, and hardcoded the word "Baseline" for the baseline load -- so a segment called 03 - New Orders in the pivot, in the bridge and on the Baseline page read as "Baseline" in the one place you go to check a number before changing it. label comes first now, the same precedence pf_segment uses, in the ledger and the bridge alike. logMeta did not carry label at all, which is why neither could reach it. The immovable rows split one line per segment. Combined, "01 - YTD Sales · 02 - Open Orders" said 1.6m was untouchable without saying how much of it was billed and how much was booked -- different things a forecaster treats differently. The FINAL badge also gains the space it was missing, having rendered as "02 - Open Ordersfinal". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9424d9c42
commit
2f862a6c8b
@ -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}`
|
||||
|
||||
@ -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 && (
|
||||
<tr className="text-amber-700">
|
||||
<td className="pr-3 whitespace-nowrap max-w-[16rem] truncate" title={exclName}>
|
||||
{exclName}
|
||||
<span className="ml-1 px-1 py-0.5 rounded bg-amber-50 text-amber-700 text-[10px] uppercase tracking-wide">
|
||||
{exclLines.map(seg => (
|
||||
<tr key={seg.name} className="text-amber-700">
|
||||
<td className="pr-3 whitespace-nowrap max-w-[16rem] truncate" title={seg.name}>
|
||||
{seg.name}
|
||||
<span className="ml-1.5 px-1 py-0.5 rounded bg-amber-50 text-amber-700 text-[10px] uppercase tracking-wide">
|
||||
final
|
||||
</span>
|
||||
</td>
|
||||
{measures.map(m => (
|
||||
<td key={m.key} className={`${numCell} text-amber-700`}>
|
||||
{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)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
))}
|
||||
|
||||
{hasExcl && (
|
||||
<tr className={onTotal ? 'font-semibold text-gray-700' : 'text-gray-600'}>
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user