Show where a scale edit lands, for all three measures at once

Each ledger row derived only from its own input, so typing a dollar figure
left the units and price rows blank -- which are precisely the two numbers
that say whether you just asked for a volume change or a price change. The
plug toggle decided it and then showed you nothing.

Adds a Result line under the edit rows carrying value, units and price
together, resolved the same way the server resolves the increments: a price
input multiplies through volume, a dollar input with plug = volume scales
units in proportion, anything else holds units. Measures that actually move
are picked out in blue against the ones that hold, so plug = price reads as
price moving alone and plug = volume as price standing still.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-16 23:21:53 -04:00
parent 546af67639
commit de6064b4f2

View File

@ -296,6 +296,51 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
} }
const dollarsOnly = filled('value') && !filled('units') && !filled('price') const dollarsOnly = filled('value') && !filled('units') && !filled('price')
// Where the edit lands, for all three measures at once. The rows above each
// show only their own input, so a dollar figure entered with plug = volume
// left units and price blank -- exactly the two numbers you need to see to
// know whether you asked for a price change or a volume change.
const numOf = (key, field) => {
const raw = derived[key]?.[field]
const n = parseFloat(raw)
return isFinite(n) ? n : null
}
const outcome = (() => {
const curValue = basisOf('value') ?? 0
const curUnits = basisOf('units') ?? 0
if (!filled('value') && !filled('units') && !filled('price')) return null
let newValue, newUnits
if (filled('price') && !filled('value')) {
// price is the input; dollars fall out of price x volume
const targetPrice = numOf('price', 'new')
newUnits = filled('units') ? numOf('units', 'new') : curUnits
newValue = targetPrice != null && newUnits != null ? targetPrice * newUnits : null
} else {
newValue = filled('value') ? numOf('value', 'new') : curValue
if (filled('units')) newUnits = numOf('units', 'new')
else if (dollarsOnly && scalePlug === 'volume') newUnits = curValue === 0 ? null
: curUnits * (newValue / curValue)
else newUnits = curUnits
}
if (newValue == null || newUnits == null) return null
const newPrice = newUnits === 0 ? null : newValue / newUnits
const curPriceLocal = curUnits === 0 ? null : curValue / curUnits
return {
value: newValue,
units: newUnits,
price: newPrice,
moved: {
value: Math.abs(newValue - curValue) > 1e-9,
units: Math.abs(newUnits - curUnits) > 1e-9,
price: curPriceLocal != null && newPrice != null && Math.abs(newPrice - curPriceLocal) > 1e-9,
},
}
})()
const setEdit = (key, field, raw) => const setEdit = (key, field, raw) =>
setScaleInputs(prev => ({ ...prev, [key]: { field, raw } })) setScaleInputs(prev => ({ ...prev, [key]: { field, raw } }))
@ -407,6 +452,22 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
})} })}
</tr> </tr>
))} ))}
{outcome && (
<>
<tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}</tr>
<tr className="font-semibold text-gray-700">
<td className="pr-3 whitespace-nowrap">Result</td>
{measures.map(m => (
<td key={m.key} className={numCell}>
<span className={outcome.moved[m.key] ? 'text-blue-600' : 'text-gray-400'}>
{fmtNum(outcome[m.key], m.dp)}
</span>
</td>
))}
</tr>
</>
)}
</tbody> </tbody>
</table> </table>