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:
@@ -191,6 +191,235 @@ export function useMetrics(uuid: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Energy / Pricing hooks — typed TanStack Query wrappers for /api/energy/*.
|
||||
//
|
||||
// Query-key conventions:
|
||||
// ['energy-contracts'] — contract list
|
||||
// ['energy-contract', id] — single contract with versions
|
||||
// ['energy-profiles'] — pricing profile structures
|
||||
// ['energy-prices', start, end] — price curve
|
||||
// ['energy-costs', start, end, limit] — cost periods
|
||||
// ['energy-costs-summary', start, end] — summary
|
||||
// ['dsmr-latest'] — DSMR latest
|
||||
// ===========================================================================
|
||||
|
||||
// Re-exported energy types for consumers
|
||||
export type ContractResponse = components['schemas']['ContractResponse']
|
||||
export type ContractDetailResponse = components['schemas']['ContractDetailResponse']
|
||||
export type ContractVersionResponse = components['schemas']['ContractVersionResponse']
|
||||
export type ContractListResponse = components['schemas']['ContractListResponse']
|
||||
export type ContractCreate = components['schemas']['ContractCreate']
|
||||
export type ContractPatch = components['schemas']['ContractPatch']
|
||||
export type VersionCreate = components['schemas']['VersionCreate']
|
||||
export type ProfilesResponse = components['schemas']['ProfilesResponse']
|
||||
export type PricesResponse = components['schemas']['PricesResponse']
|
||||
export type PricePointSchema = components['schemas']['PricePointSchema']
|
||||
export type ManualTariffSchema = components['schemas']['ManualTariffSchema']
|
||||
export type CostsResponse = components['schemas']['CostsResponse']
|
||||
export type CostPeriodSchema = components['schemas']['CostPeriodSchema']
|
||||
export type SummaryResponse = components['schemas']['SummaryResponse']
|
||||
export type DsmrLatestResponse = components['schemas']['DsmrLatestResponse']
|
||||
export type TibberTestResponse = components['schemas']['TibberTestResponse']
|
||||
export type TibberTestPriceSchema = components['schemas']['TibberTestPriceSchema']
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: list all energy contracts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useContracts() {
|
||||
return useQuery({
|
||||
queryKey: ['energy-contracts'],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/contracts')
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: single contract with full version history
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useContractDetail(id: number) {
|
||||
return useQuery({
|
||||
queryKey: ['energy-contract', id],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/contracts/{contract_id}', {
|
||||
params: { path: { contract_id: id } },
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: energy pricing profile structures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnergyProfiles() {
|
||||
return useQuery({
|
||||
queryKey: ['energy-profiles'],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/profiles')
|
||||
return res.data
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation: create energy contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useCreateContract() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: ContractCreate) =>
|
||||
apiClient.POST('/api/energy/contracts', { body }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['energy-contracts'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation: update (PATCH) energy contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useUpdateContract() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: number; body: ContractPatch }) =>
|
||||
apiClient.PATCH('/api/energy/contracts/{contract_id}', {
|
||||
params: { path: { contract_id: id } },
|
||||
body,
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['energy-contracts'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation: add a new version to an existing contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useAddContractVersion() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: number; body: VersionCreate }) =>
|
||||
apiClient.POST('/api/energy/contracts/{contract_id}/versions', {
|
||||
params: { path: { contract_id: id } },
|
||||
body,
|
||||
}),
|
||||
onSuccess: (_data, vars) => {
|
||||
void qc.invalidateQueries({ queryKey: ['energy-contracts'] })
|
||||
void qc.invalidateQueries({ queryKey: ['energy-contract', vars.id] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: energy price curve (Tibber 15-min / manual tariff)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnergyPrices(start?: string, end?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['energy-prices', start, end],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/prices', {
|
||||
params: {
|
||||
query: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: cost periods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnergyCosts(start?: string, end?: string, limit?: number) {
|
||||
return useQuery({
|
||||
queryKey: ['energy-costs', start, end, limit],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/costs', {
|
||||
params: {
|
||||
query: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
...(limit != null ? { limit } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: cost summary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnergyCostSummary(start?: string, end?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['energy-costs-summary', start, end],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/costs/summary', {
|
||||
params: {
|
||||
query: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: DSMR latest reading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useDsmrLatest() {
|
||||
return useQuery({
|
||||
queryKey: ['dsmr-latest'],
|
||||
queryFn: async () => {
|
||||
const res = await apiClient.GET('/api/energy/dsmr/latest')
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation: recompute costs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useRecomputeCosts() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ start, end }: { start?: string; end?: string } = {}) => {
|
||||
// The schema marks start/end as required, but the backend accepts them as
|
||||
// optional query params; we spread only defined values.
|
||||
const query = {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
} as { start: string; end: string }
|
||||
return apiClient.POST('/api/energy/costs/recompute', {
|
||||
params: { query },
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['energy-costs'] })
|
||||
void qc.invalidateQueries({ queryKey: ['energy-costs-summary'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query: time-range readings for a device (window + limit — never full-table)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user