Summen-Statistik der Kategorien

Eigenes 'Löschen' PopUp
This commit is contained in:
2026-03-01 11:48:24 +00:00
parent ed6bc21248
commit 2a9ae7e806
6 changed files with 181 additions and 117 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN_HAUSHALT, ZAHLUNGSARTEN_PRIVAT, MonthlyStats } from '@/types/ausgaben';
import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN_HAUSHALT, ZAHLUNGSARTEN_PRIVAT } from '@/types/ausgaben';
import { Category } from '@/app/api/categories/route';
interface AusgabenFormProps {
@@ -28,12 +28,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
const [isSubmitting, setIsSubmitting] = useState(false);
const [editId, setEditId] = useState<number | null>(null);
// Monthly stats
const [stats, setStats] = useState<MonthlyStats | null>(null);
const [month, setMonth] = useState('');
const [year, setYear] = useState('');
const [isLoadingStats, setIsLoadingStats] = useState(false);
// Autocomplete data
const [autoCompleteWo, setAutoCompleteWo] = useState<string[]>([]);
const [autoCompleteWas, setAutoCompleteWas] = useState<string[]>([]);
@@ -41,23 +35,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
const [katDropdownOpen, setKatDropdownOpen] = useState(false);
const katDropdownRef = useRef<HTMLDivElement>(null);
const fetchStats = useCallback(async (y: string, m: string) => {
if (!y || !m) return;
setIsLoadingStats(true);
try {
const response = await fetch(`/api/ausgaben/stats?year=${y}&month=${m}&typ=${typ}`);
const data = await response.json();
if (data.success) {
setStats(data.data);
}
} catch (error) {
console.error('Error fetching stats:', error);
} finally {
setIsLoadingStats(false);
}
}, [typ]);
const fetchAutoComplete = useCallback(async () => {
try {
const response = await fetch(`/api/ausgaben/autocomplete?typ=${typ}`);
@@ -71,22 +48,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
}
}, [typ]);
// Initialize month/year on first load
useEffect(() => {
const now = new Date();
const currentMonth = String(now.getMonth() + 1).padStart(2, '0');
const currentYear = String(now.getFullYear());
setMonth(currentMonth);
setYear(currentYear);
}, []);
// Fetch stats when month, year, or typ changes
useEffect(() => {
if (month && year) {
fetchStats(year, month);
}
}, [month, year, typ, fetchStats]);
// Fetch autocomplete data when typ changes
useEffect(() => {
fetchAutoComplete();
@@ -111,21 +72,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
.catch(() => {});
}, []);
const handleMonthChange = (newMonth: string) => {
setMonth(newMonth);
};
const handleYearChange = (newYear: string) => {
setYear(newYear);
};
const formatAmount = (amount: number | null) => {
if (amount === null || amount === undefined) return '0,00 €';
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(amount);
};
useEffect(() => {
if (selectedEntry) {
@@ -211,7 +157,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Wo: formData.Wo,
Was: formData.Was,
Kat: formData.Kat,
Wieviel: formData.Wieviel,
Wieviel: String(formData.Wieviel).replace(',', '.'),
Wie: formData.Wie,
TYP: formData.TYP,
};
@@ -227,8 +173,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
if (response.ok) {
handleReset();
onSuccess();
// Refresh stats after successful save
fetchStats(year, month);
} else {
alert('Fehler beim Speichern!');
}
@@ -355,11 +299,14 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
</td>
<td className="p-2 w-24">
<input
type="number"
step="0.01"
type="text"
inputMode="decimal"
value={formData.Wieviel}
onChange={(e) => setFormData({ ...formData, Wieviel: e.target.value })}
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
onChange={(e) => {
const val = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.');
setFormData({ ...formData, Wieviel: val });
}}
className="w-full px-2 py-1 text-base rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
placeholder="0.00"
required
/>
@@ -405,53 +352,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
Löschen
</button>
</div>
{/* Monatsstatistiken */}
<div className="mt-6 pt-4 pb-6 -mb-6 border-t border-black -mx-6 px-6 bg-[#E0E0FF]">
<div className="flex items-center justify-between pt-1">
<div className="flex gap-4 items-center">
<label className="font-semibold">Monat:</label>
<select
value={month}
onChange={(e) => handleMonthChange(e.target.value)}
className="border border-gray-400 rounded px-3 py-1"
>
<option value="01">Januar</option>
<option value="02">Februar</option>
<option value="03">März</option>
<option value="04">April</option>
<option value="05">Mai</option>
<option value="06">Juni</option>
<option value="07">Juli</option>
<option value="08">August</option>
<option value="09">September</option>
<option value="10">Oktober</option>
<option value="11">November</option>
<option value="12">Dezember</option>
</select>
<label className="font-semibold">Jahr:</label>
<input
type="number"
value={year}
onChange={(e) => handleYearChange(e.target.value)}
className="border border-gray-400 rounded px-3 py-1 w-24"
min="2013"
max="2099"
/>
</div>
<div>
{isLoadingStats ? (
<span>Lade...</span>
) : stats ? (
<span className="font-bold text-lg">
Summe: {formatAmount(stats.totalAusgaben)}
</span>
) : null}
</div>
</div>
</div>
</form>
</div>
);

View File

@@ -1,5 +1,6 @@
'use client';
import { useState } from 'react';
import { AusgabenEntry } from '@/types/ausgaben';
interface AusgabenListProps {
@@ -9,14 +10,12 @@ interface AusgabenListProps {
}
export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenListProps) {
const handleDelete = async (id: number) => {
if (!confirm('Wirklich löschen?')) return;
const [confirmId, setConfirmId] = useState<number | null>(null);
const handleDeleteConfirmed = async (id: number) => {
setConfirmId(null);
try {
const response = await fetch(`/api/ausgaben/${id}`, {
method: 'DELETE',
});
const response = await fetch(`/api/ausgaben/${id}`, { method: 'DELETE' });
if (response.ok) {
onDelete(id);
} else {
@@ -83,7 +82,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
Bearbeiten
</button>
<button
onClick={() => handleDelete(entry.ID)}
onClick={() => setConfirmId(entry.ID)}
className="text-red-600 hover:text-red-800 px-3 py-1 rounded text-sm"
>
Löschen
@@ -94,6 +93,29 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
)}
</tbody>
</table>
{/* Bestätigungs-Modal */}
{confirmId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white border-2 border-black rounded-lg shadow-xl p-6 w-80">
<p className="text-lg font-semibold mb-6 text-center">Eintrag wirklich löschen?</p>
<div className="flex justify-center gap-4">
<button
onClick={() => handleDeleteConfirmed(confirmId)}
className="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-6 rounded-lg transition-colors"
>
Löschen
</button>
<button
onClick={() => setConfirmId(null)}
className="bg-gray-200 hover:bg-gray-300 text-black font-medium py-2 px-6 rounded-lg transition-colors"
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,120 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { MonthlyStats } from '@/types/ausgaben';
import { Category } from '@/app/api/categories/route';
interface MonatsStatistikProps {
typ: number;
refreshKey?: number;
}
export default function MonatsStatistik({ typ, refreshKey }: MonatsStatistikProps) {
const [stats, setStats] = useState<MonthlyStats | null>(null);
const [month, setMonth] = useState('');
const [year, setYear] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
// Initialize month/year
useEffect(() => {
const now = new Date();
setMonth(String(now.getMonth() + 1).padStart(2, '0'));
setYear(String(now.getFullYear()));
}, []);
// Fetch categories once
useEffect(() => {
fetch('/api/categories')
.then((r) => r.json())
.then((data) => { if (data.success) setCategories(data.data); })
.catch(() => {});
}, []);
const fetchStats = useCallback(async (y: string, m: string) => {
if (!y || !m) return;
setIsLoading(true);
try {
const response = await fetch(`/api/ausgaben/stats?year=${y}&month=${m}&typ=${typ}`);
const data = await response.json();
if (data.success) setStats(data.data);
} catch (error) {
console.error('Error fetching stats:', error);
} finally {
setIsLoading(false);
}
}, [typ]);
useEffect(() => {
if (month && year) fetchStats(year, month);
}, [month, year, typ, refreshKey, fetchStats]);
const formatAmount = (amount: number) =>
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount);
const getCatLabel = (code: string) => {
const cat = categories.find((c) => c.value === code);
return cat ? `${cat.label}` : code;
};
return (
<div className="mt-4 bg-[#E0E0FF] border border-black rounded-lg shadow-md p-4">
{/* Zeile 1: Monat/Jahr + Gesamtsumme */}
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex gap-4 items-center">
<label className="font-semibold">Monat:</label>
<select
value={month}
onChange={(e) => setMonth(e.target.value)}
className="border border-gray-400 rounded px-3 py-1"
>
<option value="01">Januar</option>
<option value="02">Februar</option>
<option value="03">März</option>
<option value="04">April</option>
<option value="05">Mai</option>
<option value="06">Juni</option>
<option value="07">Juli</option>
<option value="08">August</option>
<option value="09">September</option>
<option value="10">Oktober</option>
<option value="11">November</option>
<option value="12">Dezember</option>
</select>
<label className="font-semibold">Jahr:</label>
<input
type="number"
value={year}
onChange={(e) => setYear(e.target.value)}
className="border border-gray-400 rounded px-3 py-1 w-24"
min="2013"
max="2099"
/>
</div>
<div>
{isLoading ? (
<span>Lade...</span>
) : stats ? (
<span className="font-bold text-lg">
Summe: {formatAmount(stats.totalAusgaben)}
</span>
) : null}
</div>
</div>
{/* Zeile 2+: Kategorien */}
{!isLoading && stats?.katStats && Object.keys(stats.katStats).length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-400 flex flex-wrap gap-x-6 gap-y-1">
{Object.entries(stats.katStats).map(([code, total]) => (
<div key={code} className="flex gap-2 text-sm">
<span className="font-medium">{getCatLabel(code)}:</span>
<span>{formatAmount(total)}</span>
</div>
))}
</div>
)}
</div>
);
}