274 lines
8.3 KiB
TypeScript
274 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import Highcharts from 'highcharts';
|
|
import HighchartsReact from 'highcharts-react-official';
|
|
import type { Options } from 'highcharts';
|
|
import { WerteEntry } from '@/types/werte';
|
|
import TabLayout from '@/components/TabLayout';
|
|
|
|
const COLOR_ZUCKER = '#e67e22';
|
|
const COLOR_DRUCKS = '#c0392b';
|
|
const COLOR_DRUCKD = '#2980b9';
|
|
const COLOR_PULS = '#27ae60';
|
|
const COLOR_GEWICHT = '#8e44ad';
|
|
|
|
function getDefaultDates() {
|
|
const to = new Date();
|
|
const from = new Date();
|
|
from.setDate(from.getDate() - 30);
|
|
return {
|
|
from: from.toISOString().split('T')[0],
|
|
to: to.toISOString().split('T')[0],
|
|
};
|
|
}
|
|
|
|
function toTimestamp(datum: string, zeit: string): number {
|
|
return new Date(`${datum}T${zeit}`).getTime();
|
|
}
|
|
|
|
function hasValue(v: number | string | null | undefined): boolean {
|
|
if (v == null || v === '') return false;
|
|
const n = parseFloat(String(v));
|
|
return !isNaN(n) && n !== 0;
|
|
}
|
|
|
|
const baseChartOptions: Partial<Options> = {
|
|
chart: {
|
|
type: 'line',
|
|
backgroundColor: '#F4F4F5',
|
|
style: { fontFamily: 'Arial, Helvetica, sans-serif' },
|
|
height: 300,
|
|
width: null,
|
|
},
|
|
xAxis: {
|
|
type: 'datetime',
|
|
dateTimeLabelFormats: {
|
|
day: '%d.%m.',
|
|
week: '%d.%m.',
|
|
month: '%m.%Y',
|
|
},
|
|
title: { text: null },
|
|
labels: { style: { fontSize: '11px' } },
|
|
},
|
|
tooltip: {
|
|
xDateFormat: '%d.%m.%Y %H:%M',
|
|
shared: true,
|
|
valueDecimals: 1,
|
|
},
|
|
credits: { enabled: false },
|
|
legend: { enabled: true },
|
|
plotOptions: {
|
|
line: {
|
|
lineWidth: 2,
|
|
marker: { enabled: true, radius: 2, symbol: 'circle' },
|
|
},
|
|
},
|
|
};
|
|
|
|
export default function ChartsClient() {
|
|
const defaults = getDefaultDates();
|
|
const [from, setFrom] = useState(defaults.from);
|
|
const [to, setTo] = useState(defaults.to);
|
|
const [entries, setEntries] = useState<WerteEntry[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchData = useCallback(async (fromDate: string, toDate: string) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(`/api/werte?from=${fromDate}&to=${toDate}`, {
|
|
cache: 'no-store',
|
|
headers: { 'Cache-Control': 'no-cache' },
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setEntries(data.data);
|
|
} else {
|
|
setError('Fehler beim Laden der Daten.');
|
|
}
|
|
} catch {
|
|
setError('Netzwerkfehler.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchData(from, to);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const handleApply = () => fetchData(from, to);
|
|
|
|
// --- Build data series (sorted asc, null/zero values excluded) ---
|
|
const zuckerData: [number, number][] = entries
|
|
.filter(e => hasValue(e.Zucker))
|
|
.map(e => [toTimestamp(e.Datum, e.Zeit), parseFloat(String(e.Zucker))]);
|
|
|
|
const druckSData: [number, number][] = entries
|
|
.filter(e => hasValue(e.DruckS))
|
|
.map(e => [toTimestamp(e.Datum, e.Zeit), parseFloat(String(e.DruckS))]);
|
|
|
|
const druckDData: [number, number][] = entries
|
|
.filter(e => hasValue(e.DruckD))
|
|
.map(e => [toTimestamp(e.Datum, e.Zeit), parseFloat(String(e.DruckD))]);
|
|
|
|
const pulsData: [number, number][] = entries
|
|
.filter(e => hasValue(e.Puls))
|
|
.map(e => [toTimestamp(e.Datum, e.Zeit), parseFloat(String(e.Puls))]);
|
|
|
|
const gewichtData: [number, number][] = entries
|
|
.filter(e => hasValue(e.Gewicht))
|
|
.map(e => [toTimestamp(e.Datum, e.Zeit), parseFloat(String(e.Gewicht))]);
|
|
|
|
// --- Chart options ---
|
|
const zuckerOptions: Options = {
|
|
...baseChartOptions,
|
|
title: { text: 'Blutzucker', style: { fontWeight: 'bold', fontSize: '15px' } },
|
|
yAxis: {
|
|
title: { text: 'mmol/L' },
|
|
// min: 50,
|
|
softMin: 100,
|
|
softMax: 170,
|
|
gridLineColor: '#ddd',
|
|
},
|
|
series: [
|
|
{
|
|
type: 'line',
|
|
name: 'Zucker',
|
|
data: zuckerData,
|
|
color: COLOR_ZUCKER,
|
|
},
|
|
],
|
|
} as Options;
|
|
|
|
const druckOptions: Options = {
|
|
...baseChartOptions,
|
|
title: { text: 'Blutdruck', style: { fontWeight: 'bold', fontSize: '15px' } },
|
|
yAxis: {
|
|
title: { text: 'mmHg' },
|
|
softMin: 100,
|
|
softMax: 160,
|
|
gridLineColor: '#ddd',
|
|
},
|
|
series: [
|
|
{
|
|
type: 'line',
|
|
name: 'Druck sys',
|
|
data: druckSData,
|
|
color: COLOR_DRUCKS,
|
|
},
|
|
{
|
|
type: 'line',
|
|
name: 'Druck dia',
|
|
data: druckDData,
|
|
color: COLOR_DRUCKD,
|
|
},
|
|
],
|
|
} as Options;
|
|
|
|
const pulsOptions: Options = {
|
|
...baseChartOptions,
|
|
title: { text: 'Puls', style: { fontWeight: 'bold', fontSize: '15px' } },
|
|
yAxis: {
|
|
title: { text: 'bpm' },
|
|
softMin: 70,
|
|
softMax: 90,
|
|
gridLineColor: '#ddd',
|
|
},
|
|
series: [
|
|
{
|
|
type: 'line',
|
|
name: 'Puls',
|
|
data: pulsData,
|
|
color: COLOR_PULS,
|
|
},
|
|
],
|
|
} as Options;
|
|
|
|
const gewichtOptions: Options = {
|
|
...baseChartOptions,
|
|
title: { text: 'Gewicht', style: { fontWeight: 'bold', fontSize: '15px' } },
|
|
yAxis: {
|
|
title: { text: 'kg' },
|
|
softMin: 70,
|
|
softMax: 90,
|
|
gridLineColor: '#ddd',
|
|
},
|
|
series: [
|
|
{
|
|
type: 'line',
|
|
name: 'Gewicht',
|
|
data: gewichtData,
|
|
color: COLOR_GEWICHT,
|
|
},
|
|
],
|
|
} as Options;
|
|
|
|
return (
|
|
<TabLayout>
|
|
|
|
{/* Date range picker */}
|
|
<div className="bg-white border border-black rounded-lg p-4 mb-6 flex flex-wrap gap-4 items-end">
|
|
<div className="flex flex-row items-center gap-2">
|
|
<label className="font-semibold text whitespace-nowrap" htmlFor="from">
|
|
Von:
|
|
</label>
|
|
<input
|
|
id="from"
|
|
type="date"
|
|
value={from}
|
|
onChange={e => setFrom(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"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-row items-center gap-2">
|
|
<label className="font-semibold text whitespace-nowrap" htmlFor="to">
|
|
Bis:
|
|
</label>
|
|
<input
|
|
id="to"
|
|
type="date"
|
|
value={to}
|
|
onChange={e => setTo(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"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleApply}
|
|
disabled={isLoading}
|
|
className="bg-[#85B7D7] hover:bg-[#6a9fc5] ml-20 px-5 py-2 text-sm rounded-lg transition-colors shadow-md disabled:opacity-50"
|
|
>
|
|
{isLoading ? 'Lade...' : 'Anzeigen'}
|
|
</button>
|
|
{error && <span className="text-red-600 text-sm">{error}</span>}
|
|
{!isLoading && !error && entries.length === 0 && (
|
|
<span className="text-gray-500 text-sm">Keine Daten im gewählten Zeitraum.</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
{isLoading ? (
|
|
<div className="text-center py-16 text-gray-500">Lade Daten…</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="block w-full bg-white border border-black rounded-lg shadow-md p-3 overflow-hidden">
|
|
<HighchartsReact highcharts={Highcharts} options={zuckerOptions} containerProps={{ style: { display: 'block', width: '100%' } }} />
|
|
</div>
|
|
<div className="block w-full bg-white border border-black rounded-lg shadow-md p-3 overflow-hidden">
|
|
<HighchartsReact highcharts={Highcharts} options={druckOptions} containerProps={{ style: { display: 'block', width: '100%' } }} />
|
|
</div>
|
|
<div className="block w-full bg-white border border-black rounded-lg shadow-md p-3 overflow-hidden">
|
|
<HighchartsReact highcharts={Highcharts} options={pulsOptions} containerProps={{ style: { display: 'block', width: '100%' } }} />
|
|
</div>
|
|
<div className="block w-full bg-white border border-black rounded-lg shadow-md p-3 overflow-hidden">
|
|
<HighchartsReact highcharts={Highcharts} options={gewichtOptions} containerProps={{ style: { display: 'block', width: '100%' } }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</TabLayout>
|
|
);
|
|
}
|