'use client'; import { useState, useEffect, useCallback } from 'react'; import AppLayout from '@/components/AppLayout'; import BarChart from '@/components/BarChart'; import { VerbrauchPoint, Every, RangeKey } from '@/types/strom'; const COLOR_24H = '#2980b9'; const COLOR_7D = '#27ae60'; const COLOR_31D = '#e67e22'; const COLOR_365D = '#8e44ad'; type SelectorKind = 'date24' | 'date' | 'month' | 'year'; function todayStr(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function thisMonthStr(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } function fmtDate(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function addDays(dateStr: string, delta: number): string { const d = dateStr ? new Date(`${dateStr}T00:00:00`) : new Date(); d.setDate(d.getDate() + delta); return fmtDate(d); } function addMonths(monthStr: string, delta: number): string { const [y, m] = (monthStr || thisMonthStr()).split('-').map(Number); const d = new Date(y, m - 1 + delta, 1); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } const CURRENT_YEAR = new Date().getFullYear(); const YEARS = Array.from({ length: 11 }, (_, i) => CURRENT_YEAR - i); interface ChartCardProps { range: RangeKey; every: Every; title: string; color: string; selector: SelectorKind; } function ChartCard({ range, every, title, color, selector }: ChartCardProps) { const [value, setValue] = useState(() => { switch (selector) { case 'date24': return ''; // leer = rollierend letzte 24h case 'date': return todayStr(); case 'month': return thisMonthStr(); case 'year': return String(CURRENT_YEAR); } }); const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // E-Auto-Ladetage (nur 31d): werden asynchron nachgeladen const [ladeDays, setLadeDays] = useState>(new Set()); const [ladeLoading, setLadeLoading] = useState(false); const [ladeMeta, setLadeMeta] = useState<{ thresholdKw: number; minHours: number } | null>(null); const fetchLaden = useCallback( async (sel: string) => { if (range !== '31d') return; setLadeLoading(true); setLadeDays(new Set()); try { const res = await fetch(`/api/laden?month=${sel}`, { cache: 'no-store' }); const json = await res.json(); if (json.success) { setLadeDays(new Set(json.days)); setLadeMeta({ thresholdKw: json.thresholdKw, minHours: json.minHours }); } } catch { // Markierung ist sekundaer -> Fehler still ignorieren } finally { setLadeLoading(false); } }, [range] ); const fetchData = useCallback( async (sel: string) => { setIsLoading(true); setError(null); try { const params = new URLSearchParams({ range }); if (selector === 'date24' && sel) params.set('date', sel); if (selector === 'date') params.set('date', sel); if (selector === 'month') params.set('month', sel); if (selector === 'year') params.set('year', sel); const res = await fetch(`/api/verbrauch?${params.toString()}`, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); if (json.success) { // nur valide [number, number]-Tupel uebernehmen (robust gegen // unerwartete Elemente, damit die Darstellung nie crasht) const clean: VerbrauchPoint[] = Array.isArray(json.data) ? (json.data as unknown[]).filter( (p): p is VerbrauchPoint => Array.isArray(p) && p.length >= 2 && typeof p[0] === 'number' && typeof p[1] === 'number' ) : []; setData(clean); } else { setError(json.error || 'Fehler beim Laden der Daten.'); } } catch { setError('Netzwerkfehler.'); } finally { setIsLoading(false); } }, [range, selector] ); useEffect(() => { fetchData(value); fetchLaden(value); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleApply = () => { fetchData(value); fetchLaden(value); }; // Navigation per Pfeil: einen Tag/Monat/Jahr zurueck (-1) oder vor (+1) const step = (delta: number) => { let nv: string; if (selector === 'month') nv = addMonths(value, delta); else if (selector === 'year') nv = String(Number(value || CURRENT_YEAR) + delta); else nv = addDays(value, delta); // date24 + date setValue(nv); fetchData(nv); fetchLaden(nv); }; // Vorwaerts nicht in die Zukunft const canForward = selector === 'month' ? (value || thisMonthStr()) < thisMonthStr() : selector === 'year' ? Number(value || CURRENT_YEAR) < CURRENT_YEAR : (value || todayStr()) < todayStr(); // Jahres-Chart: Achse immer ueber das komplette Kalenderjahr -> alle 12 Monate // sichtbar, Restmonate bleiben ohne Balken. let xMin: number | undefined; let xMax: number | undefined; if (range === '365d') { const y = Number(value); if (Number.isFinite(y)) { xMin = new Date(y, 0, 1).getTime(); // bis Ende Dezember -> Januar des Folgejahres wird nicht mehr angezeigt xMax = new Date(y + 1, 0, 1).getTime() - 1; } } // Gesamtverbrauch des angezeigten Zeitraums const total = data.reduce((sum, [, v]) => sum + v, 0); const totalLabel = range === '365d' ? `Gesamtverbrauch ${value}` : 'Gesamtverbrauch'; return (
{/* Steuerung */}
{/* Pfeil zurueck */} {selector === 'year' ? ( ) : ( setValue(e.target.value)} className="border border-gray-400 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> )} {/* Pfeil vorwaerts (nicht in die Zukunft) */}
{error && {error}} {!isLoading && !error && data.length === 0 && ( Keine Daten im gewählten Zeitraum. )}
{/* Diagramm */} {isLoading ? (
Lade Daten…
) : ( <>
{/* Legende: E-Auto-Ladetage (nur 31d) */} {range === '31d' ? ( {ladeLoading ? 'E-Auto-Ladung wird geprüft…' : ladeDays.size > 0 ? `E-Auto geladen (> ${ladeMeta?.thresholdKw ?? ''} kW, ≥ ${ladeMeta?.minHours ?? ''} h)` : 'Keine E-Auto-Ladung erkannt'} ) : ( )} {data.length > 0 && ( {totalLabel}:{' '} {total.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} kWh )}
)}
); } export default function VerbrauchCharts() { return ( ); }