From 19fda276b031b05bc8886c85008eec4555ba84e2 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 16 Mar 2026 17:14:17 +0100 Subject: [PATCH] Admin Dashboard mit Chart.js: Balkendiagramm zeigt Proben pro Tierarzt --- frontend/package-lock.json | 30 ++++ frontend/package.json | 2 + frontend/src/pages/AdminDashboardPage.tsx | 159 +++++++++++++++++----- frontend/src/styles/global.css | 16 +++ 4 files changed, 170 insertions(+), 37 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dff6c22..875e8c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,9 @@ "name": "muh-frontend", "version": "0.0.1", "dependencies": { + "chart.js": "^4.5.1", "react": "18.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "18.2.0", "react-router-dom": "6.23.1" }, @@ -745,6 +747,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -1290,6 +1298,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1538,6 +1558,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2b1410c..8f48839 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "preview": "vite preview" }, "dependencies": { + "chart.js": "^4.5.1", "react": "18.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "18.2.0", "react-router-dom": "6.23.1" }, diff --git a/frontend/src/pages/AdminDashboardPage.tsx b/frontend/src/pages/AdminDashboardPage.tsx index 289f3ff..ba9b2b4 100644 --- a/frontend/src/pages/AdminDashboardPage.tsx +++ b/frontend/src/pages/AdminDashboardPage.tsx @@ -1,7 +1,27 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; +import { Bar } from "react-chartjs-2"; import { apiGet } from "../lib/api"; +// Chart.js Komponenten registrieren +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + interface VetSampleStats { userId: string; displayName: string; @@ -36,6 +56,99 @@ export default function AdminDashboardPage() { void loadStats(); }, []); + // Chart Daten vorbereiten + const chartData = { + labels: stats?.samplesPerVet.map((vet) => vet.displayName) || [], + datasets: [ + { + label: "Anzahl Proben", + data: stats?.samplesPerVet.map((vet) => vet.sampleCount) || [], + backgroundColor: "rgba(90, 123, 168, 0.8)", + borderColor: "rgba(90, 123, 168, 1)", + borderWidth: 1, + borderRadius: 8, + borderSkipped: false, + }, + ], + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + title: { + display: true, + text: "Proben pro Tierarzt", + font: { + size: 16, + weight: "bold" as const, + }, + padding: { + top: 10, + bottom: 20, + }, + color: "#1d2428", + }, + tooltip: { + backgroundColor: "rgba(29, 36, 40, 0.9)", + padding: 12, + cornerRadius: 8, + titleFont: { + size: 14, + }, + bodyFont: { + size: 13, + }, + callbacks: { + label: (context: { parsed: { y: number } }) => { + return `${context.parsed.y} Proben`; + }, + }, + }, + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, + color: "#666", + }, + grid: { + color: "rgba(0, 0, 0, 0.05)", + }, + title: { + display: true, + text: "Anzahl Proben", + color: "#666", + font: { + size: 12, + }, + }, + }, + x: { + ticks: { + color: "#666", + maxRotation: 45, + minRotation: 45, + }, + grid: { + display: false, + }, + title: { + display: true, + text: "Tierarzt", + color: "#666", + font: { + size: 12, + }, + }, + }, + }, + }; + return (
{/* Header Bereich */} @@ -69,45 +182,17 @@ export default function AdminDashboardPage() { - {/* Tabelle: Proben pro Tierarzt */} + {/* Chart Bereich */}
-
-
-

Statistik

-

Proben pro Tierarzt

-
+
+ {loading ? ( +
Chart wird geladen...
+ ) : stats?.samplesPerVet.length === 0 ? ( +
Noch keine Proben vorhanden.
+ ) : ( + + )}
- - {loading ? ( -
Statistiken werden geladen...
- ) : stats?.samplesPerVet.length === 0 ? ( -
Noch keine Proben vorhanden.
- ) : ( -
- - - - - - - - - - {stats?.samplesPerVet.map((vet) => ( - - - - - - ))} - -
TierarztFirmaAnzahl Proben
- {vet.displayName} - {vet.companyName ?? "-"} - {vet.sampleCount} -
-
- )}
{/* Admin Module Grid */} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index a110be0..b5c1652 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1315,6 +1315,22 @@ a { text-align: center; } +/* Chart Container */ +.chart-container { + position: relative; + height: 400px; + padding: 20px; + background: rgba(255, 255, 255, 0.5); + border-radius: var(--radius-xl); +} + +@media (max-width: 768px) { + .chart-container { + height: 300px; + padding: 10px; + } +} + .dialog-backdrop { position: fixed; inset: 0;