Admin Dashboard mit Chart.js: Balkendiagramm zeigt Proben pro Tierarzt
This commit is contained in:
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user