add sum of balance

This commit is contained in:
Elias Bennour 2025-05-21 23:38:50 +02:00
parent 692bebdaf7
commit 380084b675
2 changed files with 49 additions and 20 deletions

View File

@ -24,6 +24,7 @@ interface AdminUser {
interface AdminUsersApiResponse {
users: AdminUser[];
totalUsers: number;
totalBalance: string; // Gesamtsaldo
}
interface AdminTransaction {
@ -49,11 +50,11 @@ interface AdminUserTransactionsApiResponse {
totalTransactions: number;
}
interface SetApprovalStatusApiResponse { // Typ für die Antwort der /set-approval-status API
interface SetApprovalStatusApiResponse {
message: string;
userId: string;
isApproved: boolean;
approvedAt: Date | null; // API liefert Date-Objekt, im Frontend ggf. als String behandeln
approvedAt: Date | null;
}
@ -65,6 +66,7 @@ const AdminPanelPage: NextPage = () => {
const [isLoadingUsers, setIsLoadingUsers] = useState(true);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [totalBalance, setTotalBalance] = useState<string | null>(null); // State für Gesamtsaldo
const [selectedUserForAdjustment, setSelectedUserForAdjustment] = useState<AdminUser | null>(null);
const [adjustmentAmount, setAdjustmentAmount] = useState<string>('');
@ -103,10 +105,10 @@ const AdminPanelPage: NextPage = () => {
const data: AdminUsersApiResponse = await res.json();
const formattedUsers = data.users.map(user => ({
...user,
// approvedAt von der API kommt als ISO-String oder null
approvedAt: user.approvedAt ? new Date(user.approvedAt).toLocaleString('de-DE') : null
}));
setUsers(formattedUsers);
setTotalBalance(data.totalBalance); // Gesamtsaldo setzen
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
@ -218,7 +220,6 @@ const AdminPanelPage: NextPage = () => {
}
};
// NEU: Benutzer Freigabestatus umschalten
const handleToggleApproval = async (userId: string, currentApprovalStatus: boolean) => {
setError(null);
setSuccessMessage(null);
@ -230,14 +231,14 @@ const AdminPanelPage: NextPage = () => {
const response = await fetch(`/api/admin/users/${userId}/set-approval-status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isApproved: !currentApprovalStatus }), // Den entgegengesetzten Status senden
body: JSON.stringify({ isApproved: !currentApprovalStatus }),
});
const data: SetApprovalStatusApiResponse = await response.json(); // Typ für die Antwort verwenden
const data: SetApprovalStatusApiResponse = await response.json();
if (!response.ok) {
throw new Error(data.message || `Fehler beim ${actionText} des Benutzers.`);
}
setSuccessMessage(data.message);
await fetchUsers(); // Benutzerliste neu laden, um den aktualisierten Status anzuzeigen
await fetchUsers();
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
@ -270,7 +271,7 @@ const AdminPanelPage: NextPage = () => {
<header className="mb-8 flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-800">Admin Panel</h1>
<p className="text-gray-600">Benutzerverwaltung</p>
<p className="text-gray-600">Benutzerverwaltung & Übersicht</p>
</div>
<div>
<button
@ -291,6 +292,29 @@ const AdminPanelPage: NextPage = () => {
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md">{error}</div>}
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md">{successMessage}</div>}
{/* Gesamtübersicht Sektion */}
<section className="mb-8 p-6 bg-white shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Gesamtübersicht</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="p-4 bg-indigo-50 rounded-lg">
<h3 className="text-sm font-medium text-indigo-700">Anzahl Benutzer</h3>
<p className="mt-1 text-3xl font-semibold text-indigo-600">{users.length}</p>
</div>
<div className="p-4 bg-green-50 rounded-lg">
<h3 className="text-sm font-medium text-green-700">Gesamtsaldo</h3>
{isLoadingUsers ? (
<p className="mt-1 text-3xl font-semibold text-green-600">Lade...</p>
) : (
<p className={`mt-1 text-3xl font-semibold ${totalBalance && parseFloat(totalBalance) < 0 ? 'text-red-600' : 'text-green-600'}`}>
{totalBalance !== null ? `${totalBalance}` : 'N/A'}
</p>
)}
</div>
{/* Hier könnten weitere Kacheln für andere Statistiken hinzukommen */}
</div>
</section>
<section className="p-6 bg-white shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Benutzerübersicht</h2>
{isLoadingUsers ? (
@ -334,7 +358,7 @@ const AdminPanelPage: NextPage = () => {
<button
onClick={() => handleToggleApproval(user.id, user.isApproved)}
className="text-yellow-600 hover:text-yellow-900"
disabled={user.id === session?.user?.id && users.filter(u => u.role === 'admin' && u.isApproved).length <= 1 && user.role === 'admin'} // Verhindere Selbstsperrung des letzten Admins
disabled={user.id === session?.user?.id && users.filter(u => u.role === 'admin' && u.isApproved).length <= 1 && user.role === 'admin'}
>
Sperren
</button>
@ -355,7 +379,7 @@ const AdminPanelPage: NextPage = () => {
)}
</section>
{/* Modal für Saldo anpassen */}
{/* Modals (bleiben unverändert) */}
<Transition appear show={isAdjustModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => setIsAdjustModalOpen(false)}>
<Transition.Child
@ -448,7 +472,6 @@ const AdminPanelPage: NextPage = () => {
</Dialog>
</Transition>
{/* Modal für Transaktionsverlauf */}
<Transition appear show={isTransactionsModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => setIsTransactionsModalOpen(false)}>
<Transition.Child
@ -528,7 +551,6 @@ const AdminPanelPage: NextPage = () => {
</div>
</Dialog>
</Transition>
</div>
);
};

View File

@ -3,11 +3,9 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]'; // Pfad zu deiner authOptions anpassen
import prisma from '../../../lib/prisma'; // Pfad zu deinem Prisma Client Singleton anpassen
// User-Typ von Prisma wird hier nicht mehr direkt für ApiAdminUser benötigt,
// da wir den Typ explizit definieren.
import { Decimal } from '@prisma/client/runtime/library'; // Import Decimal
// Definiere den Typ für die Benutzerdaten, wie sie von dieser API gesendet werden.
// Dieser Typ enthält genau die Felder, die wir auswählen und transformieren.
type ApiAdminUser = {
id: string;
name: string | null;
@ -19,7 +17,7 @@ type ApiAdminUser = {
updatedAt: string; // Datum als ISO-String
isApproved: boolean; // Freigabestatus
approvedAt: string | null; // Freigabedatum als ISO-String oder null
_count?: { // _count ist optional und enthält die optionale Anzahl der Transaktionen
_count?: {
transactions?: number;
};
};
@ -27,6 +25,7 @@ type ApiAdminUser = {
interface AdminUsersApiResponse {
users: ApiAdminUser[];
totalUsers: number;
totalBalance: string; // NEU: Gesamtsumme aller Salden als String
}
interface ErrorResponse {
@ -49,6 +48,7 @@ export default async function handler(
}
try {
// Benutzerdaten abrufen
const usersFromDb = await prisma.user.findMany({
select: {
id: true,
@ -70,8 +70,14 @@ export default async function handler(
},
});
// Konvertiere Decimal-Saldo und DateTime-Objekte in Strings für die JSON-Antwort
// und stelle sicher, dass die Struktur dem ApiAdminUser-Typ entspricht.
// NEU: Gesamtsaldo berechnen
const totalBalanceResult = await prisma.user.aggregate({
_sum: {
balance: true, // Summiere das 'balance'-Feld
},
});
const totalBalanceDecimal = totalBalanceResult._sum.balance ?? new Decimal(0);
const apiAdminUsers: ApiAdminUser[] = usersFromDb.map(user => ({
id: user.id,
name: user.name,
@ -83,14 +89,15 @@ export default async function handler(
updatedAt: user.updatedAt.toISOString(),
isApproved: user.isApproved,
approvedAt: user.approvedAt ? user.approvedAt.toISOString() : null,
_count: { // Stelle sicher, dass _count immer ein Objekt ist, auch wenn transactions 0 ist
transactions: user._count?.transactions ?? 0 // Fallback auf 0, falls undefined
_count: {
transactions: user._count?.transactions ?? 0
}
}));
return res.status(200).json({
users: apiAdminUsers,
totalUsers: apiAdminUsers.length,
totalBalance: totalBalanceDecimal.toFixed(2), // Gesamtsaldo als String hinzufügen
});
} catch (error: unknown) {