Bereichswahl dazu

This commit is contained in:
2026-04-08 09:08:24 +02:00
parent d4a5f1b1c9
commit 6c45f260c6
4 changed files with 510 additions and 59 deletions

View File

@@ -9,7 +9,19 @@ function App() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [lastUpdate, setLastUpdate] = useState(null)
const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d'
const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d', oder {type: 'custom', start, end, days}
// Handler für Zeitbereich-Änderungen
const handleTimeRangeChange = (range, customParams) => {
if (range === 'custom' && customParams) {
const start = new Date(customParams.start)
const end = new Date(customParams.end)
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24))
setTimeRange({ type: 'custom', start: customParams.start, end: customParams.end, days })
} else {
setTimeRange(range)
}
}
useEffect(() => {
const fetchData = async () => {
@@ -30,26 +42,44 @@ function App() {
let weatherUrl, rainUrl
const baseUrl = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
switch (timeRange) {
case '24h':
weatherUrl = `${baseUrl}/weather/history?hours=24`
rainUrl = null
break
case '7d':
weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=7`
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
break
case '30d':
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=30`
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
break
case '365d':
weatherUrl = `${baseUrl}/weather/daily-aggregated?days=365`
rainUrl = `${baseUrl}/weather/rain-weekly?days=365`
break
default:
weatherUrl = `${baseUrl}/weather/history?hours=24`
// Benutzerdefinierter Zeitbereich
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const start = encodeURIComponent(timeRange.start)
const end = encodeURIComponent(timeRange.end)
const days = timeRange.days || 1
if (days >= 7) {
// >= 7 Tage: Tagesaggregation mit Min/Max verwenden
weatherUrl = `${baseUrl}/weather/daily-aggregated-range?start=${start}&end=${end}`
rainUrl = null // TODO: Regen-Aggregation für Range implementieren
} else {
// < 7 Tage: Stundenaggregation verwenden
weatherUrl = `${baseUrl}/weather/hourly-aggregated-range?start=${start}&end=${end}`
rainUrl = null
}
} else {
// Vordefinierte Zeitbereiche
switch (timeRange) {
case '24h':
weatherUrl = `${baseUrl}/weather/history?hours=24`
rainUrl = null
break
case '7d':
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=7`
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
break
case '30d':
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=30`
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
break
case '365d':
weatherUrl = `${baseUrl}/weather/daily-aggregated?days=365`
rainUrl = `${baseUrl}/weather/rain-weekly?days=365`
break
default:
weatherUrl = `${baseUrl}/weather/history?hours=24`
rainUrl = null
}
}
// Wetterdaten laden
@@ -155,7 +185,7 @@ function App() {
currentData={currentWeatherData}
rainData={rainData}
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
onTimeRangeChange={handleTimeRangeChange}
/>
</main>
</div>

View File

@@ -246,3 +246,135 @@
display: none;
}
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #333;
font-size: 1.5rem;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 600;
color: #333;
font-size: 0.95rem;
}
.form-group input[type="datetime-local"] {
padding: 0.75rem;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s ease;
}
.form-group input[type="datetime-local"]:focus {
outline: none;
border-color: #0066cc;
}
.error-message {
padding: 0.75rem;
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
color: #c00;
font-size: 0.9rem;
}
.modal-info {
padding: 0.75rem;
background: #f0f8ff;
border-radius: 6px;
font-size: 0.85rem;
color: #555;
}
.modal-info p {
margin: 0.25rem 0;
}
.modal-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.modal-buttons button {
flex: 1;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-cancel:hover {
background: #e0e0e0;
}
.btn-apply {
background: #0066cc;
color: white;
}
.btn-apply:hover {
background: #0052a3;
}
@media (max-width: 768px) {
.modal-content {
padding: 1.5rem;
}
.modal-content h2 {
font-size: 1.25rem;
}
.modal-buttons {
flex-direction: column;
}
}

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official'
import { format } from 'date-fns'
@@ -21,6 +21,86 @@ Highcharts.setOptions({
})
const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange }) => {
// State für benutzerdefinierten Zeitbereich
const [showCustomRangeModal, setShowCustomRangeModal] = useState(false)
const [customStartDate, setCustomStartDate] = useState('')
const [customEndDate, setCustomEndDate] = useState('')
const [customError, setCustomError] = useState('')
// Handler für benutzerdefinierten Zeitbereich
const handleOpenCustomRange = () => {
// Versuche gespeicherten Zeitbereich zu laden
try {
const savedRange = localStorage.getItem('customTimeRange')
if (savedRange) {
const { start, end } = JSON.parse(savedRange)
setCustomStartDate(start)
setCustomEndDate(end)
} else {
// Setze Standardwerte (letzte 7 Tage)
const end = new Date()
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000)
setCustomStartDate(format(start, "yyyy-MM-dd'T'HH:mm"))
setCustomEndDate(format(end, "yyyy-MM-dd'T'HH:mm"))
}
} catch (e) {
// Bei Fehler: Standardwerte verwenden
const end = new Date()
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000)
setCustomStartDate(format(start, "yyyy-MM-dd'T'HH:mm"))
setCustomEndDate(format(end, "yyyy-MM-dd'T'HH:mm"))
}
setCustomError('')
setShowCustomRangeModal(true)
}
const handleApplyCustomRange = () => {
// Validierung
const start = new Date(customStartDate)
const end = new Date(customEndDate)
if (!customStartDate || !customEndDate) {
setCustomError('Bitte Start- und Endzeit auswählen')
return
}
const diffHours = (end - start) / (1000 * 60 * 60)
const diffDays = diffHours / 24
if (diffHours < 1) {
setCustomError('Endzeit muss mindestens 1 Stunde nach der Startzeit liegen')
return
}
if (diffDays > 365) {
setCustomError('Maximaler Zeitraum ist 1 Jahr (365 Tage)')
return
}
// Zeitbereich im localStorage speichern
try {
localStorage.setItem('customTimeRange', JSON.stringify({
start: customStartDate,
end: customEndDate
}))
} catch (e) {
// Fehler beim Speichern ignorieren
console.warn('Konnte Zeitbereich nicht speichern:', e)
}
// Anwenden
onTimeRangeChange('custom', { start: customStartDate, end: customEndDate })
setShowCustomRangeModal(false)
}
const handleCancelCustomRange = () => {
setShowCustomRangeModal(false)
setCustomError('')
}
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
const sortedData = useMemo(() => {
return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
@@ -33,6 +113,11 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Zeitraum-Label
const timeRangeLabel = useMemo(() => {
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const start = new Date(timeRange.start)
const end = new Date(timeRange.end)
return `${format(start, 'dd.MM.yyyy HH:mm', { locale: de })} - ${format(end, 'dd.MM.yyyy HH:mm', { locale: de })}`
}
switch (timeRange) {
case '24h': return 'Die letzten 24 Stunden'
case '7d': return 'Die letzten 7 Tage'
@@ -44,9 +129,18 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Aggregations-Zusatz für Chart-Titel
const aggregationSuffix = useMemo(() => {
// Custom range: basierend auf days
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const days = timeRange.days || 1
if (days >= 7) {
return ' (Tagesmittel)'
} else {
return ' (Stundenmittel)'
}
}
// Vordefinierte Bereiche
switch (timeRange) {
case '7d':
return ' (Stundenmittel)'
case '30d':
case '365d':
return ' (Tagesmittel)'
@@ -57,9 +151,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Spezieller Suffix für Temperatur bei 30d/365d
const temperatureSuffix = useMemo(() => {
// Custom range: basierend auf days
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const days = timeRange.days || 1
if (days >= 7) {
return ' (Tages-Min/Max)'
}
return ''
}
// Vordefinierte Bereiche
switch (timeRange) {
case '7d':
return ' (Stundenmittel)'
case '30d':
case '365d':
return ' (Tages-Min/Max)'
@@ -70,7 +172,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Spezieller Suffix für Windböen bei 30d/365d
const windGustSuffix = useMemo(() => {
// Custom range: basierend auf days
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const days = timeRange.days || 1
if (days >= 7) {
return ' (TagesMax)'
}
return ''
}
// Vordefinierte Bereiche
switch (timeRange) {
case '7d':
case '30d':
case '365d':
return ' (TagesMax)'
@@ -81,6 +193,10 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
const getCommonOptions = () => {
// Prüfe, ob es ein custom range ist
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
const customDays = isCustomRange ? (timeRange.days || 1) : 0
// X-Achsen-Konfiguration basierend auf Zeitraum
let xAxisConfig = {
type: 'datetime',
@@ -91,35 +207,66 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Zeitspanne für X-Achse berechnen (für festen Zeitrahmen)
const now = new Date().getTime()
let xAxisMin, xAxisMax
let tooltipDateFormat = '%d.%m.%Y'
switch (timeRange) {
case '24h':
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
xAxisMin = now - 24 * 3600 * 1000
xAxisMax = now
break
case '7d':
if (isCustomRange) {
// Custom range Konfiguration - Min/Max aus Daten nehmen
if (customDays >= 7) {
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 7 * 24 * 3600 * 1000
xAxisMax = now
break
case '30d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 30 * 24 * 3600 * 1000
xAxisMax = now
break
case '365d':
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
if (sortedData.length > 0) {
xAxisMin = new Date(sortedData[0].datetime).getTime()
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
} else {
xAxisMin = null
xAxisMax = null
}
break
tooltipDateFormat = '%d.%m.%Y'
} else {
xAxisConfig.labels = { format: '{value:%d.%m %H:%M}', align: 'center' }
tooltipDateFormat = '%d.%m.%Y %H:%M'
}
// X-Achsen-Bereich aus den tatsächlichen Daten bestimmen
if (sortedData.length > 0) {
xAxisMin = new Date(sortedData[0].datetime).getTime()
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
} else {
xAxisMin = null
xAxisMax = null
}
} else {
// Vordefinierte Bereiche
switch (timeRange) {
case '24h':
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
xAxisMin = now - 24 * 3600 * 1000
xAxisMax = now
tooltipDateFormat = '%d.%m.%Y %H:%M'
break
case '7d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 7 * 24 * 3600 * 1000
xAxisMax = now
tooltipDateFormat = '%d.%m.%Y - %Hh'
break
case '30d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 30 * 24 * 3600 * 1000
xAxisMax = now
tooltipDateFormat = '%d.%m.%Y'
break
case '365d':
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
tooltipDateFormat = '%b %Y'
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
if (sortedData.length > 0) {
xAxisMin = new Date(sortedData[0].datetime).getTime()
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
} else {
xAxisMin = null
xAxisMax = null
}
break
default:
xAxisConfig.tickInterval = 4 * 3600 * 1000
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
xAxisMin = now - 24 * 3600 * 1000
xAxisMax = now
tooltipDateFormat = '%d.%m.%Y %H:%M'
}
}
// Min/Max für X-Achse setzen
@@ -147,7 +294,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
tooltip: {
shared: true,
crosshairs: true,
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y')
xDateFormat: tooltipDateFormat
},
plotOptions: {
series: {
@@ -171,8 +318,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Temperatur Chart
const temperatureOptions = useMemo(() => {
// Bei 30d und 365d: Min/Max-Temperaturen anzeigen
if (timeRange === '30d' || timeRange === '365d') {
// Prüfe, ob Min/Max-Temperaturen angezeigt werden sollen
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
const customDays = isCustomRange ? (timeRange.days || 1) : 0
const showMinMax = (timeRange === '7d' || timeRange === '30d' || timeRange === '365d') || (isCustomRange && customDays >= 7)
// Bei 7d, 30d, 365d und custom >= 7 Tage: Min/Max-Temperaturen anzeigen
if (showMinMax) {
const minTemps = sortedData.filter(item => item.min_temperature != null).map(item => item.min_temperature)
const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature)
@@ -326,7 +478,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
valueSuffix: ' %'
}
}]
}), [sortedData])
}), [sortedData, timeRange])
// Luftdruck Chart
const pressureOptions = useMemo(() => {
@@ -385,7 +537,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
}
}]
}
}, [sortedData])
}, [sortedData, timeRange])
// Regen Chart (angepasst an Zeitraum)
const rainOptions = useMemo(() => {
@@ -452,8 +604,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Windgeschwindigkeit Chart
const windSpeedOptions = useMemo(() => {
// Bei 365d nur Windgeschwindigkeit, keine Böen
const series = timeRange === '365d'
// Prüfe, ob Böen angezeigt werden sollen (nicht bei 365d oder custom >= 365 Tage)
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
const customDays = isCustomRange ? (timeRange.days || 1) : 0
const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365)
// Bei 365d und custom >= 365 Tage: nur Windgeschwindigkeit, keine Böen
const series = hideGusts
? [{
name: 'Windgeschwindigkeit',
data: sortedData
@@ -588,7 +745,15 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
}
// Zeitformat basierend auf Zeitraum
const timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
const customDays = isCustomRange ? (timeRange.days || 1) : 0
let timeFormat = 'dd.MM HH:mm'
if (isCustomRange) {
timeFormat = customDays < 7 ? 'HH:mm' : 'dd.MM HH:mm'
} else {
timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
}
// Temperatur
const minTempItem = periodData.reduce((min, item) =>
@@ -662,6 +827,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
<span className="time-range-full">365 Tage</span>
<span className="time-range-short">365d</span>
</button>
<button
className={(typeof timeRange === 'object' && timeRange.type === 'custom') ? 'active' : ''}
onClick={handleOpenCustomRange}
>
<span className="time-range-full">Bereich</span>
<span className="time-range-short">Bereich</span>
</button>
</div>
{/* Zeitraum-Beschreibung */}
@@ -747,6 +919,55 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
</div>
{/* Modal für benutzerdefinierten Zeitbereich */}
{showCustomRangeModal && (
<div className="modal-overlay" onClick={handleCancelCustomRange}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Benutzerdefinierten Zeitbereich wählen</h2>
<div className="modal-form">
<div className="form-group">
<label htmlFor="startDate">Startzeit:</label>
<input
type="datetime-local"
id="startDate"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="endDate">Endzeit:</label>
<input
type="datetime-local"
id="endDate"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
/>
</div>
{customError && (
<div className="error-message">{customError}</div>
)}
<div className="modal-info">
<p> Endzeit muss mindestens 1 Stunde nach der Startzeit liegen</p>
<p> Maximaler Zeitraum: 1 Jahr (365 Tage)</p>
</div>
<div className="modal-buttons">
<button className="btn-cancel" onClick={handleCancelCustomRange}>
Abbrechen
</button>
<button className="btn-apply" onClick={handleApplyCustomRange}>
Anwenden
</button>
</div>
</div>
</div>
</div>
)}
{/* Footer */}
<div className="dashboard-footer">
<div className="version-line">