/** * CostView — cost trends + detail + summary. * * Features: * - Date range selector: Today / This month / Custom (SegmentedControl + DateInput). * - Recharts AreaChart showing import_cost, export_revenue, net_cost per period. * - Table showing per-15min periods with time, kWh values, costs, degraded badge. * - Summary cards: metered import/export, fixed costs, credits, total payable. * - "Recompute" button with confirmation. * - Loading/error/empty states, degraded period highlighting. * * Recharts imports are isolated to this file only. */ import { useState } from 'react' import { Stack, Text, TextInput, Loader, Center, Alert, Table, Group, Badge, ScrollArea, SegmentedControl, Button, Paper, SimpleGrid, Modal, Title, } from '@mantine/core' import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts' import { useEnergyCosts, useEnergyCostSummary, useRecomputeCosts } from './hooks' import { formatLocalTime } from '../utils/datetime' // --------------------------------------------------------------------------- // Cost limit — prevent accidental full-table pulls // --------------------------------------------------------------------------- const COSTS_MAX_LIMIT = 500 // --------------------------------------------------------------------------- // Date range helpers // --------------------------------------------------------------------------- function getTodayRange(): { start: string; end: string } { // Use local date boundary so "today" matches what the user sees in the display. const now = new Date() const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) return { start: start.toISOString(), end: end.toISOString() } } function getThisMonthRange(): { start: string; end: string } { // Use local date boundary so "this month" matches what the user sees in the display. const now = new Date() const start = new Date(now.getFullYear(), now.getMonth(), 1) const end = new Date(now.getFullYear(), now.getMonth() + 1, 1) return { start: start.toISOString(), end: end.toISOString() } } // --------------------------------------------------------------------------- // Summary cards // --------------------------------------------------------------------------- interface SummaryCardProps { label: string value: string testId?: string } function SummaryCard({ label, value, testId }: SummaryCardProps) { return ( {label} {value} ) } // --------------------------------------------------------------------------- // CostView — main component // --------------------------------------------------------------------------- type RangePreset = 'today' | 'month' | 'custom' export function CostView() { const [rangePreset, setRangePreset] = useState('today') // Date strings in YYYY-MM-DD format for custom range const [customStartStr, setCustomStartStr] = useState('') const [customEndStr, setCustomEndStr] = useState('') const [showRecomputeConfirm, setShowRecomputeConfirm] = useState(false) // Compute effective date range const { start, end } = (() => { if (rangePreset === 'today') return getTodayRange() if (rangePreset === 'month') return getThisMonthRange() return { start: customStartStr ? new Date(customStartStr).toISOString() : undefined, end: customEndStr ? new Date(customEndStr).toISOString() : undefined, } })() const costsQuery = useEnergyCosts(start, end, COSTS_MAX_LIMIT) const summaryQuery = useEnergyCostSummary(start, end) const recomputeMutation = useRecomputeCosts() async function handleRecompute() { setShowRecomputeConfirm(false) await recomputeMutation.mutateAsync({ start, end }) } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- const currency = costsQuery.data?.items[0]?.currency ?? summaryQuery.data?.currency ?? 'EUR' return ( {/* Date range selector */} Date range setRangePreset(v as RangePreset)} data={[ { label: 'Today', value: 'today' }, { label: 'This month', value: 'month' }, { label: 'Custom', value: 'custom' }, ]} data-testid="cost-range-control" /> {rangePreset === 'custom' && ( setCustomStartStr(e.currentTarget.value)} data-testid="cost-custom-start" /> setCustomEndStr(e.currentTarget.value)} data-testid="cost-custom-end" /> )} {/* Summary cards */} {summaryQuery.isLoading && (
)} {summaryQuery.isError && ( Failed to load cost summary. )} {summaryQuery.data && ( Summary )} {/* Cost chart */} {costsQuery.isLoading && (
)} {costsQuery.isError && ( Failed to load cost periods. Please refresh. )} {costsQuery.data && costsQuery.data.items.length === 0 && ( No cost data available for the selected period. )} {costsQuery.data && costsQuery.data.items.length > 0 && ( <> {/* Area chart */} Cost trends ({currency}) ({ time: formatLocalTime(item.period_start), import_cost: item.import_cost, export_revenue: item.export_revenue, net_cost: item.net_cost, }))} margin={{ top: 4, right: 16, left: 0, bottom: 4 }} > v.toFixed(2)} /> [`${typeof val === 'number' ? val.toFixed(4) : String(val)} ${currency}`, undefined] } /> {/* Detail table */} Period detail Time Import kWh Export kWh Import cost Export rev. Net cost {costsQuery.data.items.map((item, idx) => ( {formatLocalTime(item.period_start)} {(item.d1_kwh + item.d2_kwh).toFixed(3)} {(item.r1_kwh + item.r2_kwh).toFixed(3)} {item.import_cost.toFixed(4)} {item.export_revenue.toFixed(4)} {item.net_cost.toFixed(4)} {item.degraded && ( degraded )} ))}
)} {/* Recompute confirmation modal */} {showRecomputeConfirm && ( setShowRecomputeConfirm(false)} title="Recompute costs?" size="sm" data-testid="recompute-confirm-modal" > This will recompute all cost periods for the selected date range. Continue? )}
) }