From 1df2d8276c2c4072421b1e93834221de692383b4 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 16 Mar 2026 17:11:17 +0100 Subject: [PATCH] =?UTF-8?q?Admin=20Dashboard=20mit=20Statistiken:=20Tier?= =?UTF-8?q?=C3=A4rzte-Anzahl,=20Gesamtproben=20und=20Proben=20pro=20Tierar?= =?UTF-8?q?zt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../muh/service/AdminStatisticsService.java | 57 ++++++++ .../muh/web/AdminController.java | 25 ++++ .../muh/web/dto/AdminStatistics.java | 17 +++ frontend/src/pages/AdminDashboardPage.tsx | 137 ++++++++++++++---- frontend/src/styles/global.css | 16 ++ 5 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 backend/src/main/java/de/svencarstensen/muh/service/AdminStatisticsService.java create mode 100644 backend/src/main/java/de/svencarstensen/muh/web/AdminController.java create mode 100644 backend/src/main/java/de/svencarstensen/muh/web/dto/AdminStatistics.java diff --git a/backend/src/main/java/de/svencarstensen/muh/service/AdminStatisticsService.java b/backend/src/main/java/de/svencarstensen/muh/service/AdminStatisticsService.java new file mode 100644 index 0000000..77d89f8 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/service/AdminStatisticsService.java @@ -0,0 +1,57 @@ +package de.svencarstensen.muh.service; + +import de.svencarstensen.muh.domain.AppUser; +import de.svencarstensen.muh.domain.Sample; +import de.svencarstensen.muh.domain.UserRole; +import de.svencarstensen.muh.repository.AppUserRepository; +import de.svencarstensen.muh.repository.SampleRepository; +import de.svencarstensen.muh.web.dto.AdminStatistics; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AdminStatisticsService { + + private final AppUserRepository appUserRepository; + private final SampleRepository sampleRepository; + + public AdminStatisticsService(AppUserRepository appUserRepository, SampleRepository sampleRepository) { + this.appUserRepository = appUserRepository; + this.sampleRepository = sampleRepository; + } + + public AdminStatistics getStatistics() { + // Alle Hauptnutzer (Tierärzte) laden - primaryUser=true und role=CUSTOMER + List vets = appUserRepository.findAll().stream() + .filter(u -> Boolean.TRUE.equals(u.primaryUser())) + .filter(u -> u.role() == UserRole.CUSTOMER) + .toList(); + + // Alle Proben laden + List allSamples = sampleRepository.findAll(); + + // Proben pro Tierarzt zählen (basierend auf createdByUserCode) + List samplesPerVet = vets.stream() + .map(vet -> { + long sampleCount = allSamples.stream() + .filter(s -> vet.id().equals(s.createdByUserCode())) + .count(); + return new AdminStatistics.VetSampleStats( + vet.id(), + vet.displayName(), + vet.companyName(), + sampleCount + ); + }) + .filter(s -> s.sampleCount() > 0) + .sorted((a, b) -> Long.compare(b.sampleCount(), a.sampleCount())) + .toList(); + + return new AdminStatistics( + vets.size(), + allSamples.size(), + samplesPerVet + ); + } +} diff --git a/backend/src/main/java/de/svencarstensen/muh/web/AdminController.java b/backend/src/main/java/de/svencarstensen/muh/web/AdminController.java new file mode 100644 index 0000000..6a7c4d4 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/web/AdminController.java @@ -0,0 +1,25 @@ +package de.svencarstensen.muh.web; + +import de.svencarstensen.muh.service.AdminStatisticsService; +import de.svencarstensen.muh.web.dto.AdminStatistics; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/admin") +@PreAuthorize("hasRole('ADMIN')") +public class AdminController { + + private final AdminStatisticsService adminStatisticsService; + + public AdminController(AdminStatisticsService adminStatisticsService) { + this.adminStatisticsService = adminStatisticsService; + } + + @GetMapping("/statistics") + public AdminStatistics getStatistics() { + return adminStatisticsService.getStatistics(); + } +} diff --git a/backend/src/main/java/de/svencarstensen/muh/web/dto/AdminStatistics.java b/backend/src/main/java/de/svencarstensen/muh/web/dto/AdminStatistics.java new file mode 100644 index 0000000..fbe0bf2 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/web/dto/AdminStatistics.java @@ -0,0 +1,17 @@ +package de.svencarstensen.muh.web.dto; + +import java.util.List; + +public record AdminStatistics( + long totalVets, + long totalSamples, + List samplesPerVet +) { + public record VetSampleStats( + String userId, + String displayName, + String companyName, + long sampleCount + ) { + } +} diff --git a/frontend/src/pages/AdminDashboardPage.tsx b/frontend/src/pages/AdminDashboardPage.tsx index 76ae228..289f3ff 100644 --- a/frontend/src/pages/AdminDashboardPage.tsx +++ b/frontend/src/pages/AdminDashboardPage.tsx @@ -1,17 +1,40 @@ +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { apiGet } from "../lib/api"; + +interface VetSampleStats { + userId: string; + displayName: string; + companyName: string | null; + sampleCount: number; +} + +interface AdminStatistics { + totalVets: number; + totalSamples: number; + samplesPerVet: VetSampleStats[]; +} export default function AdminDashboardPage() { const navigate = useNavigate(); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const adminModules = [ - { - title: "Benutzer", - description: "Freigabe und Sperre von Benutzerkonten", - icon: "👥", - route: "/admin/benutzer", - color: "#5b7ba8", - }, - ]; + useEffect(() => { + async function loadStats() { + try { + const response = await apiGet("/admin/statistics"); + setStats(response); + } catch (err) { + setError((err as Error).message); + } finally { + setLoading(false); + } + } + + void loadStats(); + }, []); return (
@@ -21,41 +44,97 @@ export default function AdminDashboardPage() {

Administration

Administrator Dashboard

- Verwalten Sie die Freigabe und Sperre von Benutzerkonten. + Übersicht über Tierärzte und Proben im System.

+ {error ? ( +
{error}
+ ) : null} + + {/* Statistik-Karten */} +
+
+ Tierärzte + + {loading ? "..." : stats?.totalVets ?? 0} + +
+
+ Proben insgesamt + + {loading ? "..." : stats?.totalSamples ?? 0} + +
+
+ + {/* Tabelle: Proben pro Tierarzt */} +
+
+
+

Statistik

+

Proben pro Tierarzt

+
+
+ + {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 */}

Verwaltung

-

Verwaltungsmodule

+

Verwaltungsmodul

- {adminModules.map((module) => ( - - ))} +
- - ); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index d7ad76a..a110be0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1299,6 +1299,22 @@ a { color: var(--muted); } +.metric-card--secondary { + background: linear-gradient(135deg, rgba(139, 90, 124, 0.2) 0%, rgba(139, 90, 124, 0.05) 100%); + border-color: rgba(139, 90, 124, 0.25); +} + +.sample-count { + display: inline-block; + min-width: 32px; + padding: 4px 12px; + background: rgba(90, 123, 168, 0.15); + border-radius: 16px; + font-weight: 600; + color: var(--text); + text-align: center; +} + .dialog-backdrop { position: fixed; inset: 0;