Tabelle hinzugefügt

This commit is contained in:
2026-04-09 16:55:02 +02:00
parent 995a4c64d8
commit 99553ad4da
4 changed files with 331 additions and 25 deletions

View File

@@ -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)}
/>
</main>
</div>

View File

@@ -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;
}
}

View File

@@ -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 (
<div className="dashboard">
{/* Navigation für Zeitraum-Auswahl */}
@@ -832,6 +937,12 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
<span className="time-range-full">Bereich</span>
<span className="time-range-short">Bereich</span>
</button>
<button
className={`table-toggle-btn${showTable ? ' active' : ''}`}
onClick={onToggleTable}
>
<span>{showTable ? 'Grafik' : 'Tabelle'}</span>
</button>
</div>
{/* Zeitraum-Beschreibung */}
@@ -839,7 +950,49 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
{timeRangeLabel}
</div>
{/* Charts Grid */}
{/* Charts Grid / Tabellenansicht */}
{showTable ? (
<div className="table-view">
<div className="table-actions no-print">
<button className="btn-print" onClick={() => window.print()}>🖨 Drucken</button>
</div>
<table className="weather-table">
<thead>
<tr>
<th rowSpan={2}>Datum</th>
<th colSpan={2}>Temperatur<br/>°C</th>
<th colSpan={2}>Feuchte<br/>%</th>
<th colSpan={2}>Luftdruck<br/>hPa</th>
<th rowSpan={2}>Regen<br/>mm</th>
<th rowSpan={2}>Wind-V<br/>max km/h</th>
</tr>
<tr>
<th>min</th>
<th>max</th>
<th>min</th>
<th>max</th>
<th>min</th>
<th>max</th>
</tr>
</thead>
<tbody>
{tableData.map((row, i) => (
<tr key={i}>
<td>{row.date}</td>
<td>{row.tempMin != null ? row.tempMin.toFixed(1) : '-'}</td>
<td>{row.tempMax != null ? row.tempMax.toFixed(1) : '-'}</td>
<td>{row.humMin != null ? row.humMin : '-'}</td>
<td>{row.humMax != null ? row.humMax : '-'}</td>
<td>{row.pressMin != null ? row.pressMin.toFixed(0) : '-'}</td>
<td>{row.pressMax != null ? row.pressMax.toFixed(0) : '-'}</td>
<td>{row.rain != null ? row.rain.toFixed(1) : '0'}</td>
<td>{row.windMax != null ? row.windMax.toFixed(1) : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="charts-grid">
<div className="chart-item">
<div className="current-value">Aktuell: {current.temperature?.toFixed(1) || '-'}°C</div>
@@ -883,7 +1036,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
<div className="chart-item">
<div className="current-value">Aktuell: {current.rain?.toFixed(1) || '-'} mm</div>
<div className="chart-container">
<h3><span>🌧 Regen{aggregationSuffix}</span><span className="unit">[mm]</span></h3>
<h3><span>🌧 Regen{rainSuffix}</span><span className="unit">[mm]</span></h3>
<div className="chart-wrapper">
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
</div>
@@ -916,6 +1069,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
</div>
</div>
)} {/* end showTable ternary */}
{/* Modal für benutzerdefinierten Zeitbereich */}
{showCustomRangeModal && (