Wire real utility cost data into Weekly Actuals report

Replaces the Utilities placeholder with a cost-breakdown table by
category (usage/standing/CCL/RAB levy/fixed extras/VAT/total) sourced
from the utilities app's internal API, plus a projected-total note for
the current month. Mirrors the existing forecasting integration
pattern (best-effort fetch, doesn't break the rest of the report on
failure). Also adds the missing UTILITIES_URL/UTILITIES_API_KEY env
vars to docker-compose.yml, needed by both this and the pre-existing
directors-forecast integration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-28 14:32:48 +00:00
parent 932302ebe2
commit dfd3a506d1
4 changed files with 142 additions and 6 deletions

View file

@ -17,6 +17,9 @@ import type { WeeklyActualData, WeekSummaryRow, MonthSplitEntry, MonthDailyEntry
const fmtCcy = (n: number | null | undefined, dp = 2) =>
n == null ? '—' : `£${n.toLocaleString('en-GB', { minimumFractionDigits: dp, maximumFractionDigits: dp })}`
const fmtUnits = (n: number | null | undefined, unit: string) =>
n == null ? '—' : `${n.toLocaleString('en-GB', { maximumFractionDigits: 1 })} ${unit}`
const fmtPct = (n: number | null | undefined) =>
n == null ? '—' : `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`
@ -470,13 +473,78 @@ function SalesHistorySection({
)
}
// ── Section: Utilities Placeholder ─────────────────────────────────────────
// ── Section: Utilities ──────────────────────────────────────────────────────
function UtilitiesSection({ utilities }: { utilities: WeeklyActualData['utilities'] }) {
const { costs, estimate, error } = utilities
if (error || !costs) {
return (
<div className="wa-utilities-placeholder">
<p>Utilities report unavailable{error ? `${error}` : ''}.</p>
</div>
)
}
if (costs.categories.length === 0) {
return (
<div className="wa-utilities-placeholder">
<p>No utility categories configured.</p>
</div>
)
}
function UtilitiesPlaceholder() {
return (
<div className="wa-utilities-placeholder">
<p>Utilities report pending meter readings app integration.</p>
</div>
<>
<div className="wa-table-wrap">
<table className="wa-table">
<thead>
<tr>
<th></th>
<th>Consumption</th>
<th>Usage</th>
<th>Standing</th>
<th>CCL</th>
<th>RAB Levy</th>
<th>Fixed Extras</th>
<th>VAT</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{costs.categories.map(c => (
<tr key={c.category}>
<td className="wa-row-label">{c.category_name}</td>
<td className="wa-num">{fmtUnits(c.consumption, c.unit_label)}</td>
<td className="wa-num">{fmtCcy(c.usage_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(c.standing_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(c.ccl_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(c.rab_levy_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy((c.metering_cost_pence + c.other_charges_cost_pence) / 100)}</td>
<td className="wa-num">{fmtCcy(c.vat_pence / 100)}</td>
<td className="wa-num">{fmtCcy(c.total_pence / 100)}</td>
</tr>
))}
<tr className="wa-total-row">
<td className="wa-row-label">Total</td>
<td className="wa-num">{costs.totals.consumption.toLocaleString('en-GB', { maximumFractionDigits: 1 })}</td>
<td className="wa-num">{fmtCcy(costs.totals.usage_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(costs.totals.standing_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(costs.totals.ccl_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy(costs.totals.rab_levy_cost_pence / 100)}</td>
<td className="wa-num">{fmtCcy((costs.totals.metering_cost_pence + costs.totals.other_charges_cost_pence) / 100)}</td>
<td className="wa-num">{fmtCcy(costs.totals.vat_pence / 100)}</td>
<td className="wa-num">{fmtCcy(costs.totals.total_pence / 100)}</td>
</tr>
</tbody>
</table>
</div>
{estimate && (
<p className="wa-note">
Projected total for the month ({estimate.period.remaining_days} day{estimate.period.remaining_days === 1 ? '' : 's'} remaining to estimate): {fmtCcy(estimate.totals.total_pence / 100)}
</p>
)}
</>
)
}
@ -580,7 +648,7 @@ export default function WeeklyActual() {
{/* ── Utilities ────────────────────────────────────────────── */}
<section className="wa-section">
<h2 className="wa-section-title">Utilities</h2>
<UtilitiesPlaceholder />
<UtilitiesSection utilities={data.utilities} />
</section>
</div>
)}