Group Reports/Estimates by category, merge extras, add data-basis column
Reports and Estimates now group meters by category with a per-category subtotal (in that category's own unit) instead of one flat list — the grand-total row no longer sums consumption across categories, since mixing kWh/m3/L is meaningless; money totals still sum fine. On Reports: CCL, RAB levy and fixed extras collapse into one "Extras" column/stat (still separately editable/stored, just merged for display). New "Basis" column classifies how each meter's figure was derived — Complete, Distributed (interpolated across a sparse-reading gap), Up to date / As of [date] (real data short of period end), or No data — via a new classifyBasis() in cost-calc.js. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7a4627520c
commit
a41b1171d3
5 changed files with 164 additions and 59 deletions
|
|
@ -105,6 +105,25 @@ async function interpolatedValueAtDate(meterId, date) {
|
|||
return { value, before, after }
|
||||
}
|
||||
|
||||
// Classifies how a period's consumption figure was actually derived, for
|
||||
// display next to a report row:
|
||||
// - 'no_data' — no readings cover the period at all
|
||||
// - 'partial' — real data only reaches as_of_date, short of periodEnd
|
||||
// (the normal case for a still-open current period)
|
||||
// - 'complete' — readings land exactly on both boundaries, no interpolation
|
||||
// needed — either a lucky manual coincidence or, once a
|
||||
// device feeds daily readings, the normal case
|
||||
// - 'distributed' — full period covered, but one or both boundaries needed
|
||||
// interpolating across a sparse-reading gap
|
||||
function classifyBasis(hasData, periodStart, periodEnd, first, last) {
|
||||
if (!hasData) return { status: 'no_data', as_of_date: null }
|
||||
const lastDate = toISODate(last.reading_date)
|
||||
if (lastDate < periodEnd) return { status: 'partial', as_of_date: lastDate }
|
||||
const firstDate = toISODate(first.reading_date)
|
||||
if (firstDate === periodStart && lastDate === periodEnd) return { status: 'complete', as_of_date: null }
|
||||
return { status: 'distributed', as_of_date: null }
|
||||
}
|
||||
|
||||
// Consumption for a meter over [periodStart, periodEnd] (inclusive). Interpolates
|
||||
// the meter's value at each boundary between the real readings bracketing it,
|
||||
// then takes the difference — so a period's consumption is prorated by days
|
||||
|
|
@ -119,17 +138,21 @@ export async function getPeriodConsumption(meterId, periodStart, periodEnd) {
|
|||
if (!startPoint || !endPoint) {
|
||||
const first = (await readingOnOrAfter(meterId, periodStart)) || (await readingOnOrBefore(meterId, periodStart))
|
||||
const last = await readingOnOrBefore(meterId, periodEnd)
|
||||
if (!first || !last || last.reading_date <= first.reading_date) {
|
||||
return { consumption: null, first, last, has_data: false }
|
||||
const hasData = !!(first && last && last.reading_date > first.reading_date)
|
||||
const basis = classifyBasis(hasData, periodStart, periodEnd, first, last)
|
||||
if (!hasData) {
|
||||
return { consumption: null, first, last, has_data: false, ...basis }
|
||||
}
|
||||
const rawConsumption = Number(last.reading_value) - Number(first.reading_value)
|
||||
const consumption = await convertMeterConsumption(meterId, rawConsumption)
|
||||
return { consumption, first, last, has_data: true }
|
||||
return { consumption, first, last, has_data: true, ...basis }
|
||||
}
|
||||
|
||||
const first = startPoint.before
|
||||
const last = endPoint.after
|
||||
const rawConsumption = endPoint.value - startPoint.value
|
||||
const consumption = await convertMeterConsumption(meterId, rawConsumption)
|
||||
return { consumption, first: startPoint.before, last: endPoint.after, has_data: true }
|
||||
return { consumption, first, last, has_data: true, ...classifyBasis(true, periodStart, periodEnd, first, last) }
|
||||
}
|
||||
|
||||
// Trailing average daily consumption as of the meter's latest reading,
|
||||
|
|
@ -391,10 +414,13 @@ async function splitCostAcrossSegments(meterId, rangeStart, rangeEnd, consumptio
|
|||
// or backdated after the fact) that falls inside the period. `asOfDate` is only
|
||||
// used as a fallback when the meter has no tariff-assignment history at all.
|
||||
export async function getMeterCostForPeriod(meterId, periodStart, periodEnd, asOfDate) {
|
||||
const { consumption, first, last, has_data } = await getPeriodConsumption(meterId, periodStart, periodEnd)
|
||||
const { consumption, first, last, has_data, status, as_of_date } = await getPeriodConsumption(meterId, periodStart, periodEnd)
|
||||
const daysInPeriod = daysInclusive(periodStart, periodEnd)
|
||||
const costed = await splitCostAcrossSegments(meterId, periodStart, periodEnd, consumption, asOfDate || periodEnd)
|
||||
return { consumption, has_data, first_reading: first, last_reading: last, days_in_period: daysInPeriod, ...costed }
|
||||
return {
|
||||
consumption, has_data, status, as_of_date,
|
||||
first_reading: first, last_reading: last, days_in_period: daysInPeriod, ...costed,
|
||||
}
|
||||
}
|
||||
|
||||
// Estimate for the remainder of an open period: actual consumption to date +
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue