diff --git a/backend/src/routes/weekly-actual.js b/backend/src/routes/weekly-actual.js index a09db3a7..51d46bbb 100644 --- a/backend/src/routes/weekly-actual.js +++ b/backend/src/routes/weekly-actual.js @@ -1,5 +1,6 @@ import { requireAuth } from '../auth.js' import { getSetting } from '../db.js' +import { getUtilityCosts, getUtilityEstimate } from '../lib/utilities-client.js' async function fcFetch(path) { const apiKey = await getSetting('forecasting_api_key') || process.env.FORECASTING_API_KEY @@ -289,6 +290,25 @@ export async function weeklyActualRoutes(fastify) { }) } + // ── Utilities (cost breakdown/estimate from the `utilities` app) ────────── + // Fetched best-effort for the report's calendar month — a failure here + // (e.g. utilities app not deployed, or key not configured) must not break + // the rest of the report. + let utilities = { costs: null, estimate: null, error: null } + try { + const period = `${year}-${String(month).padStart(2, '0')}` + const now = new Date() + const isCurrentPeriod = year === now.getFullYear() && month === now.getMonth() + 1 + + const [costs, estimate] = await Promise.all([ + getUtilityCosts(period), + isCurrentPeriod ? getUtilityEstimate() : Promise.resolve(null), + ]) + utilities = { costs, estimate, error: null } + } catch (err) { + utilities = { costs: null, estimate: null, error: err.message } + } + return { week_ending: toDateStr(weekEndDate), week_start: toDateStr(weekStartDate), @@ -300,6 +320,7 @@ export async function weeklyActualRoutes(fastify) { month_daily, monthly_trend, monthly_split, + utilities, } }) } diff --git a/docker-compose.yml b/docker-compose.yml index b4b52bf5..2722e2ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: - DATABASE_URL=${DATABASE_URL} - FORECASTING_URL=${FORECASTING_URL:-http://10.10.10.113:3080} - FORECASTING_API_KEY=${FORECASTING_API_KEY:-} + - UTILITIES_URL=${UTILITIES_URL:-http://10.10.10.127:3080} + - UTILITIES_API_KEY=${UTILITIES_API_KEY:-} - CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET} - SETTINGS_URL=${SETTINGS_URL} - SETTINGS_SECRET=${SETTINGS_SECRET} diff --git a/frontend/src/pages/WeeklyActual.tsx b/frontend/src/pages/WeeklyActual.tsx index 18aa190e..0db2fe65 100644 --- a/frontend/src/pages/WeeklyActual.tsx +++ b/frontend/src/pages/WeeklyActual.tsx @@ -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 ( +
Utilities report unavailable{error ? ` — ${error}` : ''}.
+No utility categories configured.
+Utilities report — pending meter readings app integration.
-| + | Consumption | +Usage | +Standing | +CCL | +RAB Levy | +Fixed Extras | +VAT | +Total | +
|---|---|---|---|---|---|---|---|---|
| {c.category_name} | +{fmtUnits(c.consumption, c.unit_label)} | +{fmtCcy(c.usage_cost_pence / 100)} | +{fmtCcy(c.standing_cost_pence / 100)} | +{fmtCcy(c.ccl_cost_pence / 100)} | +{fmtCcy(c.rab_levy_cost_pence / 100)} | +{fmtCcy((c.metering_cost_pence + c.other_charges_cost_pence) / 100)} | +{fmtCcy(c.vat_pence / 100)} | +{fmtCcy(c.total_pence / 100)} | +
| Total | +{costs.totals.consumption.toLocaleString('en-GB', { maximumFractionDigits: 1 })} | +{fmtCcy(costs.totals.usage_cost_pence / 100)} | +{fmtCcy(costs.totals.standing_cost_pence / 100)} | +{fmtCcy(costs.totals.ccl_cost_pence / 100)} | +{fmtCcy(costs.totals.rab_levy_cost_pence / 100)} | +{fmtCcy((costs.totals.metering_cost_pence + costs.totals.other_charges_cost_pence) / 100)} | +{fmtCcy(costs.totals.vat_pence / 100)} | +{fmtCcy(costs.totals.total_pence / 100)} | +
+ 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)} +
+ )} + > ) } @@ -580,7 +648,7 @@ export default function WeeklyActual() { {/* ── Utilities ────────────────────────────────────────────── */}