diff --git a/api/main.py b/api/main.py index c3323bf..c526ae7 100644 --- a/api/main.py +++ b/api/main.py @@ -383,35 +383,22 @@ async def get_daily_aggregated_data( conn = get_db_connection() try: with conn.cursor() as cursor: - # Bei 365 Tagen: alle verfügbaren Daten zurückgeben - if days >= 365: - cursor.execute(""" + 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, + MIN(humidity)::int as min_humidity, + MAX(humidity)::int as max_humidity, AVG(pressure)::float as pressure, + MIN(pressure)::float as min_pressure, + MAX(pressure)::float as max_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 - GROUP BY date_trunc('day', datetime) - ORDER BY datetime ASC - """) - else: - 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 + AVG(wind_dir)::float as wind_dir, + SUM(rain)::float as total_rain FROM weather_data WHERE datetime >= NOW() - make_interval(days => %s) GROUP BY date_trunc('day', datetime) @@ -439,10 +426,15 @@ async def get_daily_with_minmax_data( MIN(temperature)::float as min_temperature, MAX(temperature)::float as max_temperature, ROUND(AVG(humidity))::int as humidity, + MIN(humidity)::int as min_humidity, + MAX(humidity)::int as max_humidity, AVG(pressure)::float as pressure, + MIN(pressure)::float as min_pressure, + MAX(pressure)::float as max_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 + AVG(wind_dir)::float as wind_dir, + SUM(rain)::float as total_rain FROM weather_data WHERE datetime >= NOW() - make_interval(days => %s) GROUP BY date_trunc('day', datetime) @@ -566,10 +558,15 @@ async def get_daily_aggregated_range( MIN(temperature)::float as min_temperature, MAX(temperature)::float as max_temperature, ROUND(AVG(humidity))::int as humidity, + MIN(humidity)::int as min_humidity, + MAX(humidity)::int as max_humidity, AVG(pressure)::float as pressure, + MIN(pressure)::float as min_pressure, + MAX(pressure)::float as max_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 + AVG(wind_dir)::float as wind_dir, + SUM(rain)::float as total_rain FROM weather_data WHERE datetime BETWEEN %s AND %s GROUP BY date_trunc('day', datetime) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5e08295..0a7d301 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,7 @@ function App() { const [error, setError] = useState(null) const [lastUpdate, setLastUpdate] = useState(null) const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d', oder {type: 'custom', start, end, days} + const [showTable, setShowTable] = useState(false) // Handler für Zeitbereich-Änderungen const handleTimeRangeChange = (range, customParams) => { @@ -186,6 +187,8 @@ function App() { rainData={rainData} timeRange={timeRange} onTimeRangeChange={handleTimeRangeChange} + showTable={showTable} + onToggleTable={() => setShowTable(v => !v)} /> diff --git a/frontend/src/components/WeatherDashboard.css b/frontend/src/components/WeatherDashboard.css index 7cbe63f..4a96b53 100644 --- a/frontend/src/components/WeatherDashboard.css +++ b/frontend/src/components/WeatherDashboard.css @@ -378,3 +378,155 @@ flex-direction: column; } } + +/* ── Tabelle & Druck ──────────────────────────────────────── */ + +.table-toggle-btn { + padding: 0.5rem 1.5rem; + background: white; + border: 2px solid #0066cc; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + color: #0066cc; + transition: all 0.2s ease; +} + +.table-toggle-btn:hover { + background: #e6f0fa; +} + +.table-toggle-btn.active { + background: #0066cc; + border-color: #0066cc; + color: white; +} + +.table-view { + margin-bottom: 1.5rem; +} + +.table-actions { + display: flex; + justify-content: flex-end; + margin-bottom: 0.75rem; +} + +.btn-print { + padding: 0.45rem 1.2rem; + background: white; + border: 2px solid #555; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + color: #333; + transition: all 0.2s ease; +} + +.btn-print:hover { + background: #f0f0f0; +} + +.weather-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.weather-table th, +.weather-table td { + border-left: 1px solid rgba(255,255,255,0.3); + border-right: 1px solid rgba(255,255,255,0.3); +} + +.weather-table td { + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; +} + +.weather-table th { + background: #0066cc; + color: white; + padding: 0.6rem 0.5rem; + text-align: center; + font-weight: 600; + white-space: nowrap; + vertical-align: top; +} + +.weather-table thead tr:nth-child(2) th { + background: #3388dd; + font-weight: 400; + font-size: 0.82rem; + padding: 0.3rem 0.5rem; +} + +.weather-table thead tr:first-child th:first-child { + text-align: left; +} + +.weather-table td { + padding: 0.45rem 0.5rem; + text-align: center; + border-bottom: 1px solid #eee; + color: #333; +} + +.weather-table td:first-child { + text-align: left; + font-weight: 500; + white-space: nowrap; +} + +.weather-table tbody tr:nth-child(even) { + background: #f5f8fd; +} + +.weather-table tbody tr:hover { + background: #e6f0fa; +} + +/* Drucken */ +@media print { + .no-print, + .time-range-nav, + .time-range-label, + .dashboard-footer, + .modal-overlay { + display: none !important; + } + + body { + background: white; + } + + .dashboard { + max-width: 100%; + margin: 0; + padding: 0; + } + + .weather-table { + box-shadow: none; + font-size: 0.8rem; + } + + .weather-table th { + background: #333 !important; + color: white !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + .weather-table tbody tr:nth-child(even) { + background: #f0f0f0 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/frontend/src/components/WeatherDashboard.jsx b/frontend/src/components/WeatherDashboard.jsx index 768f8fc..737905a 100644 --- a/frontend/src/components/WeatherDashboard.jsx +++ b/frontend/src/components/WeatherDashboard.jsx @@ -20,7 +20,7 @@ Highcharts.setOptions({ } }) -const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange }) => { +const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange, showTable = false, onToggleTable }) => { // State für benutzerdefinierten Zeitbereich const [showCustomRangeModal, setShowCustomRangeModal] = useState(false) const [customStartDate, setCustomStartDate] = useState('') @@ -147,6 +147,22 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } }, [timeRange]) + // Spezieller Suffix für Regen + const rainSuffix = useMemo(() => { + if (typeof timeRange === 'object' && timeRange.type === 'custom') { + const days = timeRange.days || 1 + return days >= 7 ? ' (pro Tag)' : '' + } + switch (timeRange) { + case '7d': + case '30d': + case '365d': + return ' (pro Tag)' + default: + return '' + } + }, [timeRange]) + // Spezieller Suffix für Temperatur bei 30d/365d const temperatureSuffix = useMemo(() => { // Custom range: basierend auf days @@ -552,6 +568,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' fillColor: 'rgba(54, 162, 235, 0.3)', type: 'area', tooltip: { + valueDecimals: 1, valueSuffix: ' mm' } }, { @@ -573,6 +590,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' color: 'rgb(54, 162, 235)', type: 'column', tooltip: { + valueDecimals: 1, valueSuffix: ' mm' } }] @@ -585,6 +603,22 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' color: 'rgb(54, 162, 235)', type: 'column', tooltip: { + valueDecimals: 1, + valueSuffix: ' mm' + } + }] + } else if (typeof timeRange === 'object' && timeRange.type === 'custom') { + // Custom range: tägliche Summen aus sortedData (total_rain ist im daily-aggregated-range enthalten) + yAxisTitle = 'Regen (mm pro Tag)' + series = [{ + name: 'Regen', + data: sortedData + .filter(item => item.total_rain != null && item.total_rain > 0) + .map(item => [new Date(item.datetime).getTime(), item.total_rain]), + color: 'rgb(54, 162, 235)', + type: 'column', + tooltip: { + valueDecimals: 1, valueSuffix: ' mm' } }] @@ -793,6 +827,77 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } }, [sortedData, timeRange]) + // Regen-Lookup: date-string → total_rain + const rainByDate = useMemo(() => { + const map = {} + rainData.forEach(item => { + const key = item.date + ? item.date.split('T')[0] + : item.week_start + ? item.week_start.split('T')[0] + : null + if (key) map[key] = item.total_rain + }) + return map + }, [rainData]) + + // Tabellen-Daten: ein Eintrag pro Tag + const tableData = useMemo(() => { + const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' + const customDays = isCustomRange ? (timeRange.days || 1) : 0 + const isDailyAggregated = + ['7d', '30d', '365d'].includes(timeRange) || (isCustomRange && customDays >= 7) + + if (isDailyAggregated) { + return sortedData.map(item => { + const dateKey = item.datetime.split('T')[0] + return { + date: format(new Date(item.datetime), 'dd.MM.yyyy', { locale: de }), + tempMin: item.min_temperature ?? null, + tempMax: item.max_temperature ?? null, + humMin: item.min_humidity ?? null, + humMax: item.max_humidity ?? null, + pressMin: item.min_pressure ?? null, + pressMax: item.max_pressure ?? null, + rain: item.total_rain ?? rainByDate[dateKey] ?? null, + windMax: item.wind_gust ?? null, + } + }) + } else { + // Stundenwerte → pro Tag aggregieren + const byDay = {} + sortedData.forEach(item => { + const d = new Date(item.datetime) + const dateKey = format(d, 'yyyy-MM-dd') + const dateLabel = format(d, 'dd.MM.yyyy', { locale: de }) + if (!byDay[dateKey]) { + byDay[dateKey] = { date: dateLabel, temps: [], hums: [], pressures: [], rains: [], windGusts: [] } + } + if (item.temperature != null) byDay[dateKey].temps.push(item.temperature) + if (item.humidity != null) byDay[dateKey].hums.push(item.humidity) + if (item.pressure != null) byDay[dateKey].pressures.push(item.pressure) + if (item.rain != null) byDay[dateKey].rains.push(item.rain) + if (item.wind_gust != null) byDay[dateKey].windGusts.push(item.wind_gust) + }) + const startKey = isCustomRange ? timeRange.start.split('T')[0] : null + const endKey = isCustomRange ? timeRange.end.split('T')[0] : null + return Object.entries(byDay) + .sort(([a], [b]) => a.localeCompare(b)) + .filter(([k]) => !startKey || (k >= startKey && k <= endKey)) + .map(([, d]) => ({ + date: d.date, + tempMin: d.temps.length ? Math.min(...d.temps) : null, + tempMax: d.temps.length ? Math.max(...d.temps) : null, + humMin: d.hums.length ? Math.round(Math.min(...d.hums)) : null, + humMax: d.hums.length ? Math.round(Math.max(...d.hums)) : null, + pressMin: d.pressures.length ? Math.min(...d.pressures) : null, + pressMax: d.pressures.length ? Math.max(...d.pressures) : null, + rain: d.rains.length ? Math.max(...d.rains) : null, + windMax: d.windGusts.length ? Math.max(...d.windGusts) : null, + })) + } + }, [sortedData, rainByDate, timeRange]) + return (
{/* Navigation für Zeitraum-Auswahl */} @@ -832,6 +937,12 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' Bereich Bereich +
{/* Zeitraum-Beschreibung */} @@ -839,7 +950,49 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' {timeRangeLabel} - {/* Charts Grid */} + {/* Charts Grid / Tabellenansicht */} + {showTable ? ( +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + {tableData.map((row, i) => ( + + + + + + + + + + + + ))} + +
DatumTemperatur
°C
Feuchte
%
Luftdruck
hPa
Regen
mm
Wind-V
max km/h
minmaxminmaxminmax
{row.date}{row.tempMin != null ? row.tempMin.toFixed(1) : '-'}{row.tempMax != null ? row.tempMax.toFixed(1) : '-'}{row.humMin != null ? row.humMin : '-'}{row.humMax != null ? row.humMax : '-'}{row.pressMin != null ? row.pressMin.toFixed(0) : '-'}{row.pressMax != null ? row.pressMax.toFixed(0) : '-'}{row.rain != null ? row.rain.toFixed(1) : '0'}{row.windMax != null ? row.windMax.toFixed(1) : '-'}
+
+ ) : (
Aktuell: {current.temperature?.toFixed(1) || '-'}°C
@@ -883,7 +1036,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
Aktuell: {current.rain?.toFixed(1) || '-'} mm
-

🌧️ Regen{aggregationSuffix}[mm]

+

🌧️ Regen{rainSuffix}[mm]

@@ -916,6 +1069,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
+ )} {/* end showTable ternary */} {/* Modal für benutzerdefinierten Zeitbereich */} {showCustomRangeModal && (