diff --git a/src/lib/server/services/expenses.ts b/src/lib/server/services/expenses.ts index 5101518..a889771 100644 --- a/src/lib/server/services/expenses.ts +++ b/src/lib/server/services/expenses.ts @@ -190,17 +190,47 @@ export interface MonthlySeriesPoint { * is billed in a single currency; if mixed, the chart is still useful as a * unit-less trend line. */ +export type ChartRange = number | 'all'; + export async function monthlySeriesForProperty( companyId: string, propertyId: string, kinds: ExpenseKind[], - months = 12 + months: ChartRange = 12 ): Promise { await assertProperty(companyId, propertyId); - if (kinds.length === 0 || months < 1) return []; + if (kinds.length === 0) return []; const now = new Date(); - const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - (months - 1), 1)); + const thisMonthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); + + // Resolve "all" to the actual # of months spanning earliest expense → now. + // Cap at 120 months (10 years) so the chart stays legible and the SQL stays + // bounded even if someone imports a decade of data. + let effectiveMonths: number; + if (months === 'all') { + const [row] = await db + .select({ earliest: sql`min(${propertyExpenses.incurredAt})` }) + .from(propertyExpenses) + .where( + and( + eq(propertyExpenses.propertyId, propertyId), + inArray(propertyExpenses.kind, kinds) + ) + ); + const earliest = row?.earliest ? new Date(row.earliest) : null; + if (!earliest) return []; + const deltaMonths = + (now.getUTCFullYear() - earliest.getUTCFullYear()) * 12 + + (now.getUTCMonth() - earliest.getUTCMonth()) + + 1; + effectiveMonths = Math.max(1, Math.min(120, deltaMonths)); + } else { + if (months < 1) return []; + effectiveMonths = months; + } + + const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - (effectiveMonths - 1), 1)); const rows = await db .select({ @@ -214,7 +244,7 @@ export async function monthlySeriesForProperty( eq(propertyExpenses.propertyId, propertyId), inArray(propertyExpenses.kind, kinds), gte(propertyExpenses.incurredAt, start), - lt(propertyExpenses.incurredAt, new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))) + lt(propertyExpenses.incurredAt, new Date(Date.UTC(thisMonthStart.getUTCFullYear(), thisMonthStart.getUTCMonth() + 1, 1))) ) ) .groupBy( @@ -224,8 +254,8 @@ export async function monthlySeriesForProperty( // Build a zero-filled series across all months. const byMonth = new Map>>(); - for (let i = 0; i < months; i++) { - const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - (months - 1 - i), 1)); + for (let i = 0; i < effectiveMonths; i++) { + const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - (effectiveMonths - 1 - i), 1)); const label = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`; byMonth.set(label, {}); } diff --git a/src/routes/(app)/properties/[id]/expenses/+page.server.ts b/src/routes/(app)/properties/[id]/expenses/+page.server.ts index e1c3a7a..2d950a9 100644 --- a/src/routes/(app)/properties/[id]/expenses/+page.server.ts +++ b/src/routes/(app)/properties/[id]/expenses/+page.server.ts @@ -12,6 +12,7 @@ import { monthlySeriesForProperty, summaryForProperty, updateExpense, + type ChartRange, type ExpenseKind } from '$lib/server/services/expenses'; import type { Actions, PageServerLoad } from './$types'; @@ -58,8 +59,17 @@ function parseSettings(raw: string | null | undefined): CompanySettings { } } -export const load: PageServerLoad = async ({ locals, params }) => { +const VALID_RANGES = new Set(['6', '12', '24', 'all']); + +function parseRange(raw: string | null): ChartRange { + if (!raw || !VALID_RANGES.has(raw)) return 12; + if (raw === 'all') return 'all'; + return Number.parseInt(raw, 10) as ChartRange; +} + +export const load: PageServerLoad = async ({ locals, params, url }) => { const { company } = requireCompany(locals); + const range = parseRange(url.searchParams.get('range')); const [expenses, accounts, series, summary, [companyRow]] = await Promise.all([ listExpensesForProperty(company.id, params.id), db @@ -72,7 +82,7 @@ export const load: PageServerLoad = async ({ locals, params }) => { }) .from(propertyAccounts) .where(eq(propertyAccounts.propertyId, params.id)), - monthlySeriesForProperty(company.id, params.id, ['electricity', 'water'], 12), + monthlySeriesForProperty(company.id, params.id, ['electricity', 'water'], range), summaryForProperty(company.id, params.id, 365), db.select({ settings: companies.settings }).from(companies).where(eq(companies.id, company.id)).limit(1) ]); @@ -81,6 +91,7 @@ export const load: PageServerLoad = async ({ locals, params }) => { expenses, accounts, chartSeries: series, + chartRange: range, summary, defaultCurrency }; diff --git a/src/routes/(app)/properties/[id]/expenses/+page.svelte b/src/routes/(app)/properties/[id]/expenses/+page.svelte index 437b270..9df476b 100644 --- a/src/routes/(app)/properties/[id]/expenses/+page.svelte +++ b/src/routes/(app)/properties/[id]/expenses/+page.svelte @@ -41,13 +41,28 @@
-
+

- Electricity & water · last 12 months + Electricity & water {#if data.chartRange === 'all'}· all time{:else}· last {data.chartRange} months{/if}

- - {chartCurrency} - +
+ {chartCurrency} +
+ {#each [{ v: 6, l: '6M' }, { v: 12, l: '12M' }, { v: 24, l: '24M' }, { v: 'all', l: 'All' }] as opt} + {@const active = String(data.chartRange) === String(opt.v)} + + {opt.l} + + {/each} +
+