add custom amount option

This commit is contained in:
Elias Bennour 2025-05-22 00:19:50 +02:00
parent 1b3847b98d
commit e7693c4be1
2 changed files with 97 additions and 35 deletions

View File

@ -2,16 +2,16 @@
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
import { Decimal } from '@prisma/client/runtime/library';
import prisma from "@/lib/prisma"; // Für korrekte Decimal-Handhabung
// Definiere die erwartete Struktur des Request-Body
interface IncreaseBalanceRequestBody {
amount: number; // Der Betrag, um den erhöht werden soll (z.B. 0.10, 0.50, 1.00)
amount: number; // Kann jetzt ein beliebiger positiver Betrag oder einer der vordefinierten sein
isCustom?: boolean; // Optional: Flag, um zu signalisieren, dass es ein individueller Betrag ist
}
// Definiere die erlaubten Beträge und ihre zugehörigen Transaktionstypen
const ALLOWED_AMOUNTS: Record<number, string> = {
// Vordefinierte Beträge bleiben optional bestehen
const ALLOWED_PREDEFINED_AMOUNTS: Record<number, string> = {
0.10: "deposit_0.10",
0.20: "deposit_0.20",
0.50: "deposit_0.50",
@ -19,16 +19,27 @@ const ALLOWED_AMOUNTS: Record<number, string> = {
2.00: "deposit_2.00",
};
interface IncreaseBalanceApiResponse {
message: string;
newBalance: string;
transactionId: string;
}
interface ErrorResponse {
message: string;
allowedAmounts?: number[]; // Für den Fall, dass nur vordefinierte gemeint waren
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
res: NextApiResponse<IncreaseBalanceApiResponse | ErrorResponse>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
// Benutzer-Session abrufen, um sicherzustellen, dass der Benutzer angemeldet ist
const session = await getServerSession(req, res, authOptions);
if (!session || !session.user || !session.user.id) {
@ -38,22 +49,31 @@ export default async function handler(
const userId = session.user.id;
try {
const { amount } = req.body as IncreaseBalanceRequestBody;
const { amount, isCustom } = req.body as IncreaseBalanceRequestBody;
// Validierung des Betrags
if (typeof amount !== 'number' || !ALLOWED_AMOUNTS[amount]) {
return res.status(400).json({
message: 'Ungültiger oder nicht erlaubter Betrag.',
allowedAmounts: Object.keys(ALLOWED_AMOUNTS).map(Number),
});
if (typeof amount !== 'number' || amount <= 0) {
return res.status(400).json({ message: 'Der Betrag muss eine positive Zahl sein.' });
}
let transactionType: string;
const increaseAmountDecimal = new Decimal(amount);
const transactionType = ALLOWED_AMOUNTS[amount];
// Prisma-Transaktion verwenden, um sicherzustellen, dass beide Operationen atomar sind
if (isCustom) {
// Für individuelle Beträge
transactionType = `deposit_custom_${amount.toFixed(2)}`; // oder einfach "deposit_custom"
} else {
// Für vordefinierte Beträge
if (!ALLOWED_PREDEFINED_AMOUNTS[amount]) {
return res.status(400).json({
message: 'Ungültiger oder nicht erlaubter vordefinierter Betrag.',
allowedAmounts: Object.keys(ALLOWED_PREDEFINED_AMOUNTS).map(Number),
});
}
transactionType = ALLOWED_PREDEFINED_AMOUNTS[amount];
}
const result = await prisma.$transaction(async (tx) => {
// 1. Benutzer-Saldo aktualisieren
const updatedUser = await tx.user.update({
where: { id: userId },
data: {
@ -61,10 +81,9 @@ export default async function handler(
increment: increaseAmountDecimal,
},
},
select: { // Nur die benötigten Felder auswählen
select: {
id: true,
balance: true,
name: true,
}
});
@ -72,13 +91,12 @@ export default async function handler(
throw new Error('Benutzer nicht gefunden oder Update fehlgeschlagen.');
}
// 2. Transaktion erstellen
const newTransaction = await tx.transaction.create({
data: {
amount: increaseAmountDecimal,
type: transactionType,
userId: userId,
// description: `Einzahlung von ${amount.toFixed(2)}€`, // Optionale Beschreibung
description: `Betrag hinzugefügt: ${increaseAmountDecimal.toFixed(2)}`,
},
});
@ -86,17 +104,19 @@ export default async function handler(
});
return res.status(200).json({
message: `Saldo erfolgreich um ${amount.toFixed(2)}erhöht.`,
newBalance: result.updatedUser.balance,
message: `Betrag erfolgreich um ${amount.toFixed(2)}hinzugefügt.`,
newBalance: result.updatedUser.balance?.toFixed(2),
transactionId: result.newTransaction.id,
});
} catch (error) {
console.error('Fehler beim Erhöhen des Saldos:', error);
// Spezifischere Fehlerbehandlung basierend auf dem Fehlerobjekt
let errorMessage = 'Interner Serverfehler beim Erhöhen des Saldos.';
if (error instanceof Error && error.message.includes('Benutzer nicht gefunden')) {
return res.status(404).json({ message: 'Benutzer nicht gefunden.' });
errorMessage = 'Benutzer nicht gefunden.';
return res.status(404).json({ message: errorMessage });
}
return res.status(500).json({ message: 'Interner Serverfehler beim Erhöhen des Saldos.' });
// Hier könntest du weitere spezifische Fehlerbehandlungen hinzufügen
return res.status(500).json({ message: errorMessage });
}
}
}

View File

@ -4,7 +4,6 @@ import { useSession, signOut } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
// Typ für Transaktionen, wie sie vom Backend (API) erwartet werden
interface DashboardTransaction {
id: string;
userId: string;
@ -47,6 +46,10 @@ const DashboardPage: NextPage = () => {
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [customAmount, setCustomAmount] = useState<string>('');
const [isSubmittingCustom, setIsSubmittingCustom] = useState(false);
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin');
@ -111,16 +114,18 @@ const DashboardPage: NextPage = () => {
}
}, [status]);
const handleIncreaseBalance = async (amount: number) => {
const handleAddExpense = async (amountValue: number, isCustomAmount: boolean = false) => {
setError(null);
setSuccessMessage(null);
if (isCustomAmount) setIsSubmittingCustom(true);
try {
const response = await fetch('/api/user/increase-balance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount }),
body: JSON.stringify({ amount: amountValue, isCustom: isCustomAmount }),
});
const data: IncreaseBalanceApiResponse = await response.json();
if (!response.ok) {
@ -128,6 +133,7 @@ const DashboardPage: NextPage = () => {
}
setBalance(data.newBalance);
setSuccessMessage(data.message);
if (isCustomAmount) setCustomAmount('');
await fetchTransactions();
} catch (err: unknown) {
if (err instanceof Error) {
@ -136,9 +142,21 @@ const DashboardPage: NextPage = () => {
setError('Unbekannter Fehler beim Hinzufügen der Ausgabe aufgetreten.');
}
console.error("Fehler beim Hinzufügen:", err);
} finally {
if (isCustomAmount) setIsSubmittingCustom(false);
}
};
const handleCustomAmountSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const amountNumber = parseFloat(customAmount);
if (isNaN(amountNumber) || amountNumber <= 0) {
setError("Bitte einen gültigen positiven Betrag eingeben.");
return;
}
handleAddExpense(amountNumber, true);
};
const predefinedAmounts = [0.10, 0.20, 0.50, 1.00, 2.00];
if (status === 'loading') {
@ -159,13 +177,12 @@ const DashboardPage: NextPage = () => {
return (
<div className="min-h-screen bg-gray-100 dark:bg-slate-900 p-4 sm:p-6 lg:p-8 transition-colors duration-300">
{/* Header angepasst für mobile Ansicht */}
<header className="mb-8 flex flex-col sm:flex-row sm:justify-between sm:items-center">
<div className="mb-4 sm:mb-0"> {/* Div für Willkommenstext */}
<div className="mb-4 sm:mb-0">
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Willkommen, {userName || session.user.name}!</h1>
<p className="text-gray-600 dark:text-gray-400">Deine Strichliste</p>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4"> {/* Container für Buttons */}
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4">
{session?.user?.role === 'admin' && (
<button
onClick={() => router.push('/admin')}
@ -199,17 +216,42 @@ const DashboardPage: NextPage = () => {
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Ausgaben hinzufügen</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4">
<div className="mb-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4">
{predefinedAmounts.map((amount) => (
<button
key={amount}
onClick={() => handleIncreaseBalance(amount)}
onClick={() => handleAddExpense(amount)}
className="px-4 py-3 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150 dark:bg-indigo-500 dark:hover:bg-indigo-600"
>
+ {amount.toFixed(2)}
</button>
))}
</div>
<form onSubmit={handleCustomAmountSubmit} className="mt-6 border-t border-gray-200 dark:border-gray-700 pt-6">
<label htmlFor="customAmount" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Individueller Betrag:
</label>
<div className="mt-1 flex rounded-md shadow-sm">
<input
type="number"
name="customAmount"
id="customAmount"
step="0.01"
min="0.01"
value={customAmount}
onChange={(e) => setCustomAmount(e.target.value)}
className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-slate-700 dark:text-gray-100 px-3 py-2"
placeholder="z.B. 1.25"
/>
<button
type="submit"
disabled={isSubmittingCustom || !customAmount}
className="inline-flex items-center px-4 py-2 border border-l-0 border-indigo-600 dark:border-indigo-500 rounded-r-md bg-indigo-600 dark:bg-indigo-500 text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50"
>
Hinzufügen
</button>
</div>
</form>
</section>
<section className="p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">