Admin Dashboard mit Chart.js: Balkendiagramm zeigt Proben pro Tierarzt

This commit is contained in:
2026-03-16 17:14:17 +01:00
parent 1df2d8276c
commit 19fda276b0
4 changed files with 170 additions and 37 deletions

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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 (
<div className="page-stack">
{/* Header Bereich */}
@@ -69,45 +182,17 @@ export default function AdminDashboardPage() {
</article>
</section>
{/* Tabelle: Proben pro Tierarzt */}
{/* Chart Bereich */}
<section className="section-card">
<div className="section-card__header">
<div>
<p className="eyebrow">Statistik</p>
<h3>Proben pro Tierarzt</h3>
</div>
<div className="chart-container">
{loading ? (
<div className="empty-state">Chart wird geladen...</div>
) : stats?.samplesPerVet.length === 0 ? (
<div className="empty-state">Noch keine Proben vorhanden.</div>
) : (
<Bar data={chartData} options={chartOptions} />
)}
</div>
{loading ? (
<div className="empty-state">Statistiken werden geladen...</div>
) : stats?.samplesPerVet.length === 0 ? (
<div className="empty-state">Noch keine Proben vorhanden.</div>
) : (
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th>Tierarzt</th>
<th>Firma</th>
<th style={{ textAlign: "right" }}>Anzahl Proben</th>
</tr>
</thead>
<tbody>
{stats?.samplesPerVet.map((vet) => (
<tr key={vet.userId}>
<td>
<strong>{vet.displayName}</strong>
</td>
<td>{vet.companyName ?? "-"}</td>
<td style={{ textAlign: "right" }}>
<span className="sample-count">{vet.sampleCount}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
{/* Admin Module Grid */}

View File

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