M6-T10: frontend contract management + price/cost views + Tibber test

- EnergyPage: Mantine Tabs (Devices kept intact + Contracts/Prices/Costs).
- ContractManager/ContractForm: list/activate/add-version + version history;
  form fields rendered dynamically from /api/energy/profiles structure.
- TibberPrices + CostView: Recharts price curve, cost trend/detail/summary,
  recompute; window-bounded, currency/units from API, empty/error/loading states.
- ConfigPage: tri-state Tibber test button (token never shown).
- hooks.ts: typed energy hooks; schema.d.ts regenerated via codegen.
This commit is contained in:
2026-06-23 23:32:39 +02:00
parent 57f2459f60
commit 96e88861d4
15 changed files with 4235 additions and 32 deletions
+68 -31
View File
@@ -11,6 +11,8 @@
* - Latest readings card per device (T07): fetches /latest and /metrics; tolerates
* missing payload keys.
* - Trend charts per device (T07): EnergyCharts component (Recharts isolated inside).
*
* M6-T10: added Tabs layout with Devices / Contracts / Prices / Costs tabs.
*/
import { useState } from 'react'
@@ -34,10 +36,14 @@ import {
SimpleGrid,
Divider,
Switch,
Tabs,
} from '@mantine/core'
import { useDevices, useDeleteDevice, useTestReadDevice, useLatestReading, useMetrics } from '../energy/hooks'
import { DeviceForm } from '../energy/DeviceForm'
import { EnergyCharts } from '../energy/EnergyCharts'
import { ContractManager } from '../energy/ContractManager'
import { TibberPrices } from '../energy/TibberPrices'
import { CostView } from '../energy/CostView'
import type { ModbusDevice, ModbusTestReadResponse, MetricInfo } from '../energy/hooks'
import { ApiError } from '../api/client'
import { formatMetricValue } from '../energy/format'
@@ -488,10 +494,10 @@ function DeviceReadingsSection({ devices }: DeviceReadingsSectionProps) {
}
// ---------------------------------------------------------------------------
// EnergyPage — top-level
// DevicesTab — self-contained devices management tab (extracted from original EnergyPage)
// ---------------------------------------------------------------------------
export function EnergyPage() {
function DevicesTab() {
const devicesQuery = useDevices()
const deleteMutation = useDeleteDevice()
@@ -534,10 +540,6 @@ export function EnergyPage() {
}
}
// ---------------------------------------------------------------------------
// Loading / error states
// ---------------------------------------------------------------------------
if (devicesQuery.isLoading) {
return (
<Center pt="xl" data-testid="energy-loading">
@@ -548,39 +550,31 @@ export function EnergyPage() {
if (devicesQuery.isError || !devicesQuery.data) {
return (
<Container size="xl" pt="xl">
<Alert color="red" data-testid="energy-load-error">
Failed to load devices. Please refresh.
</Alert>
</Container>
<Alert color="red" data-testid="energy-load-error">
Failed to load devices. Please refresh.
</Alert>
)
}
const devices = devicesQuery.data.items
// ---------------------------------------------------------------------------
// Main render
// ---------------------------------------------------------------------------
return (
<Container size="xl" pt="xl" pb="xl" data-testid="energy-page">
<Stack gap="lg">
<Group justify="space-between" align="center">
<Title order={2}>Energy Devices</Title>
<Button onClick={openCreate} data-testid="device-new-button">
New Device
</Button>
</Group>
<Stack gap="lg">
<Group justify="space-between" align="center">
<Title order={2}>Energy Devices</Title>
<Button onClick={openCreate} data-testid="device-new-button">
New Device
</Button>
</Group>
<DeviceTable
devices={devices}
onEdit={openEdit}
onDelete={openDelete}
/>
<DeviceTable
devices={devices}
onEdit={openEdit}
onDelete={openDelete}
/>
{/* Latest readings cards + trend charts (T07) */}
<DeviceReadingsSection devices={devices} />
</Stack>
{/* Latest readings cards + trend charts (T07) */}
<DeviceReadingsSection devices={devices} />
{/* Create form */}
{showCreateForm && (
@@ -609,6 +603,49 @@ export function EnergyPage() {
has409Error={delete409}
/>
)}
</Stack>
)
}
// ---------------------------------------------------------------------------
// EnergyPage — top-level with Tabs
// ---------------------------------------------------------------------------
export function EnergyPage() {
return (
<Container size="xl" pt="xl" pb="xl" data-testid="energy-page">
<Tabs defaultValue="devices">
<Tabs.List mb="lg">
<Tabs.Tab value="devices" data-testid="tab-devices">
Devices
</Tabs.Tab>
<Tabs.Tab value="contracts" data-testid="tab-contracts">
Contracts
</Tabs.Tab>
<Tabs.Tab value="prices" data-testid="tab-prices">
Prices
</Tabs.Tab>
<Tabs.Tab value="costs" data-testid="tab-costs">
Costs
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="devices" data-testid="panel-devices">
<DevicesTab />
</Tabs.Panel>
<Tabs.Panel value="contracts" data-testid="panel-contracts">
<ContractManager />
</Tabs.Panel>
<Tabs.Panel value="prices" data-testid="panel-prices">
<TibberPrices />
</Tabs.Panel>
<Tabs.Panel value="costs" data-testid="panel-costs">
<CostView />
</Tabs.Panel>
</Tabs>
</Container>
)
}