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

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