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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user