Show budget % as delta (+/-) from 100% across all views

This commit is contained in:
jtricerolph 2026-07-23 11:48:38 +00:00
parent bbc9a23da0
commit 92400dbcec
4 changed files with 15 additions and 8 deletions

View file

@ -14,6 +14,7 @@ function addDays(d: Date, n: number): Date { const r = new Date(d); r.setDate(r.
function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).getDate() } function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).getDate() }
function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` } function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` }
function pctClass(pct: number): string { return pct <= 100 ? 'pct-green' : pct <= 110 ? 'pct-amber' : 'pct-red' } function pctClass(pct: number): string { return pct <= 100 ? 'pct-green' : pct <= 110 ? 'pct-amber' : 'pct-red' }
function fmtDelta(pct: number): string { const d = pct - 100; return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` }
const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d'] const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d']
@ -229,7 +230,7 @@ export default function Monthly() {
<div className="label">% Budget {isCurrentMonth ? '(Forecast)' : ''}</div> <div className="label">% Budget {isCurrentMonth ? '(Forecast)' : ''}</div>
<div className="value"> <div className="value">
{pctBudget != null {pctBudget != null
? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span> ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{fmtDelta(pctBudget)}</span>
: '—'} : '—'}
</div> </div>
{variance != null && ( {variance != null && (
@ -310,7 +311,7 @@ export default function Monthly() {
</td> </td>
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td> <td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
<td className="right"> <td className="right">
{dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{dp.toFixed(1)}%</span> : '—'} {dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{fmtDelta(dp)}</span> : '—'}
</td> </td>
<td className="right"> <td className="right">
{dv != null && <span className={dv > 0 ? 'variance-over' : 'variance-under'}>{dv > 0 ? '+' : ''}{fmtMoney(dv)}</span>} {dv != null && <span className={dv > 0 ? 'variance-over' : 'variance-under'}>{dv > 0 ? '+' : ''}{fmtMoney(dv)}</span>}
@ -325,7 +326,7 @@ export default function Monthly() {
<td className="right">100%</td> <td className="right">100%</td>
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td> <td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
<td className="right"> <td className="right">
{pctBudget != null ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span> : '—'} {pctBudget != null ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{fmtDelta(pctBudget)}</span> : '—'}
</td> </td>
<td className="right"> <td className="right">
{variance != null && <span className={variance > 0 ? 'variance-over' : 'variance-under'}>{variance > 0 ? '+' : ''}{fmtMoney(variance)}</span>} {variance != null && <span className={variance > 0 ? 'variance-over' : 'variance-under'}>{variance > 0 ? '+' : ''}{fmtMoney(variance)}</span>}

View file

@ -11,6 +11,7 @@ function fmt(d: Date): string {
} }
function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` } function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` }
function pctClass(p: number): string { return p <= 100 ? 'pct-green' : p <= 110 ? 'pct-amber' : 'pct-red' } function pctClass(p: number): string { return p <= 100 ? 'pct-green' : p <= 110 ? 'pct-amber' : 'pct-red' }
function fmtDelta(p: number): string { const d = p - 100; return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` }
function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).getDate() } function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).getDate() }
const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d'] const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d']
@ -182,7 +183,7 @@ export default function Rolling12Months() {
)} )}
</td> </td>
<td className="right"> <td className="right">
{pctB != null ? <span className={`pct-badge ${pctClass(pctB)}`}>{pctB.toFixed(1)}%</span> : '—'} {pctB != null ? <span className={`pct-badge ${pctClass(pctB)}`}>{fmtDelta(pctB)}</span> : '—'}
</td> </td>
<td className="right">{r.sales > 0 ? fmtMoney(r.sales) : '—'}</td> <td className="right">{r.sales > 0 ? fmtMoney(r.sales) : '—'}</td>
<td className="right">{pctS != null ? `${pctS.toFixed(1)}%` : '—'}</td> <td className="right">{pctS != null ? `${pctS.toFixed(1)}%` : '—'}</td>

View file

@ -21,6 +21,7 @@ function startOfWeek(d: Date): Date {
function daysInMonth(d: Date): number { return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() } function daysInMonth(d: Date): number { return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() }
function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` } function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` }
function pctClass(p: number): string { return p <= 100 ? 'pct-green' : p <= 110 ? 'pct-amber' : 'pct-red' } function pctClass(p: number): string { return p <= 100 ? 'pct-green' : p <= 110 ? 'pct-amber' : 'pct-red' }
function fmtDelta(p: number): string { const d = p - 100; return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` }
const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d'] const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d']
@ -189,7 +190,7 @@ export default function Rolling12Weeks() {
)} )}
</td> </td>
<td className="right"> <td className="right">
{pctB != null ? <span className={`pct-badge ${pctClass(pctB)}`}>{pctB.toFixed(1)}%</span> : '—'} {pctB != null ? <span className={`pct-badge ${pctClass(pctB)}`}>{fmtDelta(pctB)}</span> : '—'}
</td> </td>
<td className="right">{r.sales > 0 ? fmtMoney(r.sales) : '—'}</td> <td className="right">{r.sales > 0 ? fmtMoney(r.sales) : '—'}</td>
<td className="right">{pctS != null ? `${pctS.toFixed(1)}%` : '—'}</td> <td className="right">{pctS != null ? `${pctS.toFixed(1)}%` : '—'}</td>

View file

@ -41,6 +41,10 @@ function pctClass(pct: number | null): string {
if (pct <= 110) return 'pct-amber' if (pct <= 110) return 'pct-amber'
return 'pct-red' return 'pct-red'
} }
function fmtDelta(pct: number): string {
const d = pct - 100
return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`
}
export default function Weekly() { export default function Weekly() {
const [fromStr, setFromStr] = useState<string>(() => mondayOf(new Date())) const [fromStr, setFromStr] = useState<string>(() => mondayOf(new Date()))
@ -195,7 +199,7 @@ export default function Weekly() {
<div className="label">% vs Budget</div> <div className="label">% vs Budget</div>
<div className="value"> <div className="value">
{pctBudget != null {pctBudget != null
? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span> ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{fmtDelta(pctBudget)}</span>
: '—'} : '—'}
</div> </div>
</div> </div>
@ -247,7 +251,7 @@ export default function Weekly() {
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td> <td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
<td className="right"> <td className="right">
{depPct != null {depPct != null
? <span className={`pct-badge ${pctClass(depPct)}`}>{depPct.toFixed(1)}%</span> ? <span className={`pct-badge ${pctClass(depPct)}`}>{fmtDelta(depPct)}</span>
: '—'} : '—'}
</td> </td>
<td className="right">{depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}</td> <td className="right">{depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}</td>
@ -261,7 +265,7 @@ export default function Weekly() {
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td> <td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
<td className="right"> <td className="right">
{pctBudget != null {pctBudget != null
? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span> ? <span className={`pct-badge ${pctClass(pctBudget)}`}>{fmtDelta(pctBudget)}</span>
: '—'} : '—'}
</td> </td>
<td className="right">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</td> <td className="right">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</td>