diff --git a/app/api/werte/route.ts b/app/api/werte/route.ts
index 40c03b9..f143023 100644
--- a/app/api/werte/route.ts
+++ b/app/api/werte/route.ts
@@ -9,9 +9,20 @@ export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '10', 10);
-
- const sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
- const rows = await query(sql);
+ const from = searchParams.get('from');
+ const to = searchParams.get('to');
+
+ let sql: string;
+ let params: (string | number)[] = [];
+
+ if (from && to) {
+ sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} WHERE Datum BETWEEN ? AND ? ORDER BY Datum ASC, Zeit ASC`;
+ params = [from, to];
+ } else {
+ sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
+ }
+
+ const rows = await query(sql, params);
return NextResponse.json(
{ success: true, data: rows },
diff --git a/app/charts/page.tsx b/app/charts/page.tsx
new file mode 100644
index 0000000..4778d16
--- /dev/null
+++ b/app/charts/page.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+
+const ChartsClient = dynamic(() => import('@/components/ChartsClient'), {
+ ssr: false,
+ loading: () => (
+
+ Lade Grafiken…
+
+ ),
+});
+
+export default function ChartsPage() {
+ return ;
+}
diff --git a/app/globals.css b/app/globals.css
index c78599b..2c36089 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,5 +1,7 @@
/* stylelint-disable at-rule-no-unknown */
@import "tailwindcss";
+@source "../components/**/*.tsx";
+@source "../app/**/*.tsx";
:root {
--background: #ffffff;
diff --git a/app/page.tsx b/app/page.tsx
index f5a8597..eff9698 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
+import Link from 'next/link';
import WerteForm from '@/components/WerteForm';
import WerteList from '@/components/WerteList';
import { WerteEntry } from '@/types/werte';
@@ -84,7 +85,16 @@ export default function Home() {
Werte - Log
-
+
+
+ {'Verlauf ->'}
+
+
+
diff --git a/components/ChartsClient.tsx b/components/ChartsClient.tsx
new file mode 100644
index 0000000..ce4ae6a
--- /dev/null
+++ b/components/ChartsClient.tsx
@@ -0,0 +1,289 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import Link from 'next/link';
+import Highcharts from 'highcharts';
+import HighchartsReact from 'highcharts-react-official';
+import type { Options } from 'highcharts';
+import { WerteEntry } from '@/types/werte';
+import LogoutButton from '@/components/LogoutButton';
+
+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
= {
+ 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([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(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 (
+
+
+ {/* Header */}
+
+
Werte – Verlauf
+
+
+ {'<- Eingabe'}
+
+
+
+
+
+ {/* Date range picker */}
+
+
+
+ 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"
+ />
+
+
+
+ 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"
+ />
+
+
+ {error &&
{error}}
+ {!isLoading && !error && entries.length === 0 && (
+
Keine Daten im gewählten Zeitraum.
+ )}
+
+
+ {/* Charts */}
+ {isLoading ? (
+ Lade Daten…
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index 032bf09..cecd2fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,15 @@
{
"name": "werte_next",
- "version": "1.1.2",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "werte_next",
- "version": "1.1.2",
+ "version": "1.2.0",
"dependencies": {
+ "highcharts": "^12.5.0",
+ "highcharts-react-official": "^3.2.3",
"jose": "^6.1.3",
"mysql2": "^3.17.4",
"next": "16.1.6",
@@ -3961,6 +3963,35 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/highcharts": {
+ "version": "12.5.0",
+ "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.5.0.tgz",
+ "integrity": "sha512-uNSSv1KqRLNvkyXlf1FbeGWB1mJ/8/IYpVeqYiTIop5Wo8peUvNyzUfLa58vILsmXyz+XrETtgKIEOcSgBBKuQ==",
+ "license": "https://www.highcharts.com/license",
+ "peer": true,
+ "peerDependencies": {
+ "jspdf": "^3.0.0",
+ "svg2pdf.js": "^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "jspdf": {
+ "optional": true
+ },
+ "svg2pdf.js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/highcharts-react-official": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.3.tgz",
+ "integrity": "sha512-2gL8bVGe6Pf75tSe6IB5Ucd0nIOJX7ZrpttQBZVrjN2J9StdVZ12aOinXHIOFvlc6EzP8CbN13WS8NUq+9mptA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "highcharts": ">=6.0.0",
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
diff --git a/package.json b/package.json
index 7ffa60b..a3798e4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "werte_next",
- "version": "1.2.0",
+ "version": "2.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,6 +9,8 @@
"lint": "eslint"
},
"dependencies": {
+ "highcharts": "^12.5.0",
+ "highcharts-react-official": "^3.2.3",
"jose": "^6.1.3",
"mysql2": "^3.17.4",
"next": "16.1.6",