Compare commits

...

2 Commits

Author SHA1 Message Date
995a4c64d8 Start/Ende-Datum ohne Zeit 2026-04-09 09:26:18 +02:00
6c45f260c6 Bereichswahl dazu 2026-04-08 09:08:24 +02:00
4 changed files with 508 additions and 59 deletions

View File

@@ -514,6 +514,74 @@ async def get_weekly_rain_data(
conn.close() conn.close()
@app.get("/weather/hourly-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
async def get_hourly_aggregated_range(
start: datetime = Query(..., description="Startdatum (ISO 8601)"),
end: datetime = Query(..., description="Enddatum (ISO 8601)")
):
"""Gibt stündlich aggregierte Wetterdaten für einen bestimmten Zeitraum zurück"""
if start >= end:
raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
SELECT
date_trunc('hour', datetime) as datetime,
AVG(temperature)::float as temperature,
ROUND(AVG(humidity))::int as humidity,
AVG(pressure)::float as pressure,
AVG(wind_speed * 1.60934)::float as wind_speed,
MAX(wind_gust * 1.60934)::float as wind_gust,
AVG(wind_dir)::float as wind_dir
FROM weather_data
WHERE datetime BETWEEN %s AND %s
GROUP BY date_trunc('hour', datetime)
ORDER BY datetime ASC
""", (start, end))
results = cursor.fetchall()
return [dict(row) for row in results]
finally:
conn.close()
@app.get("/weather/daily-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
async def get_daily_aggregated_range(
start: datetime = Query(..., description="Startdatum (ISO 8601)"),
end: datetime = Query(..., description="Enddatum (ISO 8601)")
):
"""Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen für einen bestimmten Zeitraum zurück"""
if start >= end:
raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
SELECT
date_trunc('day', datetime) as datetime,
AVG(temperature)::float as temperature,
MIN(temperature)::float as min_temperature,
MAX(temperature)::float as max_temperature,
ROUND(AVG(humidity))::int as humidity,
AVG(pressure)::float as pressure,
AVG(wind_speed * 1.60934)::float as wind_speed,
MAX(wind_gust * 1.60934)::float as wind_gust,
AVG(wind_dir)::float as wind_dir
FROM weather_data
WHERE datetime BETWEEN %s AND %s
GROUP BY date_trunc('day', datetime)
ORDER BY datetime ASC
""", (start, end))
results = cursor.fetchall()
return [dict(row) for row in results]
finally:
conn.close()
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

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

View File

@@ -246,3 +246,135 @@
display: none; 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 Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official' import HighchartsReact from 'highcharts-react-official'
import { format } from 'date-fns' import { format } from 'date-fns'
@@ -21,6 +21,84 @@ Highcharts.setOptions({
}) })
const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange }) => { 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.split('T')[0])
setCustomEndDate(end.split('T')[0])
} 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'))
setCustomEndDate(format(end, 'yyyy-MM-dd'))
}
} 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'))
setCustomEndDate(format(end, 'yyyy-MM-dd'))
}
setCustomError('')
setShowCustomRangeModal(true)
}
const handleApplyCustomRange = () => {
if (!customStartDate || !customEndDate) {
setCustomError('Bitte Start- und Enddatum auswählen')
return
}
const diffDays = Math.floor((new Date(customEndDate) - new Date(customStartDate)) / (1000 * 60 * 60 * 24))
if (diffDays < 0) {
setCustomError('Enddatum muss nach dem Startdatum liegen')
return
}
if (diffDays > 365) {
setCustomError('Maximaler Zeitraum ist 1 Jahr (365 Tage)')
return
}
const startStr = customStartDate + 'T00:00'
const endStr = customEndDate + 'T23:59'
// 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: startStr, end: endStr })
setShowCustomRangeModal(false)
}
const handleCancelCustomRange = () => {
setShowCustomRangeModal(false)
setCustomError('')
}
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst) // Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
const sortedData = useMemo(() => { const sortedData = useMemo(() => {
return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime)) return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
@@ -33,6 +111,11 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Zeitraum-Label // Zeitraum-Label
const timeRangeLabel = useMemo(() => { 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) { switch (timeRange) {
case '24h': return 'Die letzten 24 Stunden' case '24h': return 'Die letzten 24 Stunden'
case '7d': return 'Die letzten 7 Tage' case '7d': return 'Die letzten 7 Tage'
@@ -44,9 +127,18 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Aggregations-Zusatz für Chart-Titel // Aggregations-Zusatz für Chart-Titel
const aggregationSuffix = useMemo(() => { 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) { switch (timeRange) {
case '7d': case '7d':
return ' (Stundenmittel)'
case '30d': case '30d':
case '365d': case '365d':
return ' (Tagesmittel)' return ' (Tagesmittel)'
@@ -57,9 +149,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Spezieller Suffix für Temperatur bei 30d/365d // Spezieller Suffix für Temperatur bei 30d/365d
const temperatureSuffix = useMemo(() => { 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) { switch (timeRange) {
case '7d': case '7d':
return ' (Stundenmittel)'
case '30d': case '30d':
case '365d': case '365d':
return ' (Tages-Min/Max)' return ' (Tages-Min/Max)'
@@ -70,7 +170,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Spezieller Suffix für Windböen bei 30d/365d // Spezieller Suffix für Windböen bei 30d/365d
const windGustSuffix = useMemo(() => { 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) { switch (timeRange) {
case '7d':
case '30d': case '30d':
case '365d': case '365d':
return ' (TagesMax)' return ' (TagesMax)'
@@ -81,6 +191,10 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Gemeinsame Chart-Optionen (angepasst an Zeitraum) // Gemeinsame Chart-Optionen (angepasst an Zeitraum)
const getCommonOptions = () => { 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 // X-Achsen-Konfiguration basierend auf Zeitraum
let xAxisConfig = { let xAxisConfig = {
type: 'datetime', type: 'datetime',
@@ -91,35 +205,66 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Zeitspanne für X-Achse berechnen (für festen Zeitrahmen) // Zeitspanne für X-Achse berechnen (für festen Zeitrahmen)
const now = new Date().getTime() const now = new Date().getTime()
let xAxisMin, xAxisMax let xAxisMin, xAxisMax
let tooltipDateFormat = '%d.%m.%Y'
switch (timeRange) { if (isCustomRange) {
case '24h': // Custom range Konfiguration - Min/Max aus Daten nehmen
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden if (customDays >= 7) {
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
xAxisMin = now - 24 * 3600 * 1000
xAxisMax = now
break
case '7d':
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' } xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
xAxisMin = now - 7 * 24 * 3600 * 1000 tooltipDateFormat = '%d.%m.%Y'
xAxisMax = now } else {
break xAxisConfig.labels = { format: '{value:%d.%m %H:%M}', align: 'center' }
case '30d': tooltipDateFormat = '%d.%m.%Y %H:%M'
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' } }
xAxisMin = now - 30 * 24 * 3600 * 1000 // X-Achsen-Bereich aus den tatsächlichen Daten bestimmen
xAxisMax = now if (sortedData.length > 0) {
break xAxisMin = new Date(sortedData[0].datetime).getTime()
case '365d': xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' } } else {
// Bei 365d: Min/Max aus vorhandenen Daten berechnen xAxisMin = null
if (sortedData.length > 0) { xAxisMax = null
xAxisMin = new Date(sortedData[0].datetime).getTime() }
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime() } else {
} else { // Vordefinierte Bereiche
xAxisMin = null switch (timeRange) {
xAxisMax = null case '24h':
} xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
break 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 // Min/Max für X-Achse setzen
@@ -147,7 +292,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
tooltip: { tooltip: {
shared: true, shared: true,
crosshairs: true, crosshairs: true,
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y') xDateFormat: tooltipDateFormat
}, },
plotOptions: { plotOptions: {
series: { series: {
@@ -171,8 +316,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Temperatur Chart // Temperatur Chart
const temperatureOptions = useMemo(() => { const temperatureOptions = useMemo(() => {
// Bei 30d und 365d: Min/Max-Temperaturen anzeigen // Prüfe, ob Min/Max-Temperaturen angezeigt werden sollen
if (timeRange === '30d' || timeRange === '365d') { 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 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) const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature)
@@ -326,7 +476,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
valueSuffix: ' %' valueSuffix: ' %'
} }
}] }]
}), [sortedData]) }), [sortedData, timeRange])
// Luftdruck Chart // Luftdruck Chart
const pressureOptions = useMemo(() => { const pressureOptions = useMemo(() => {
@@ -385,7 +535,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
} }
}] }]
} }
}, [sortedData]) }, [sortedData, timeRange])
// Regen Chart (angepasst an Zeitraum) // Regen Chart (angepasst an Zeitraum)
const rainOptions = useMemo(() => { const rainOptions = useMemo(() => {
@@ -452,8 +602,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
// Windgeschwindigkeit Chart // Windgeschwindigkeit Chart
const windSpeedOptions = useMemo(() => { const windSpeedOptions = useMemo(() => {
// Bei 365d nur Windgeschwindigkeit, keine Böen // Prüfe, ob Böen angezeigt werden sollen (nicht bei 365d oder custom >= 365 Tage)
const series = timeRange === '365d' 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', name: 'Windgeschwindigkeit',
data: sortedData data: sortedData
@@ -588,7 +743,15 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
} }
// Zeitformat basierend auf Zeitraum // 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 // Temperatur
const minTempItem = periodData.reduce((min, item) => const minTempItem = periodData.reduce((min, item) =>
@@ -662,6 +825,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
<span className="time-range-full">365 Tage</span> <span className="time-range-full">365 Tage</span>
<span className="time-range-short">365d</span> <span className="time-range-short">365d</span>
</button> </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> </div>
{/* Zeitraum-Beschreibung */} {/* Zeitraum-Beschreibung */}
@@ -747,6 +917,55 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
</div> </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">Startdatum:</label>
<input
type="date"
id="startDate"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="endDate">Enddatum:</label>
<input
type="date"
id="endDate"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
/>
</div>
{customError && (
<div className="error-message">{customError}</div>
)}
<div className="modal-info">
<p> Enddatum muss nach dem Startdatum 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 */} {/* Footer */}
<div className="dashboard-footer"> <div className="dashboard-footer">
<div className="version-line"> <div className="version-line">