diff --git a/app/api/ausgaben/stats/route.ts b/app/api/ausgaben/stats/route.ts index 88a06eb..002e055 100644 --- a/app/api/ausgaben/stats/route.ts +++ b/app/api/ausgaben/stats/route.ts @@ -56,6 +56,21 @@ export async function GET(request: Request) { const data = rows[0] || {}; + // Per-category breakdown + const [katRows] = await pool.query( + `SELECT Kat, SUM(Wieviel) as total + FROM Ausgaben + WHERE YEAR(Datum) = ? AND MONTH(Datum) = ? AND TYP = ? + GROUP BY Kat + HAVING total > 0 + ORDER BY total DESC`, + [year, month, parseInt(typ)] + ); + const katStats: Record = {}; + for (const row of katRows) { + katStats[row.Kat] = parseFloat(row.total) || 0; + } + // Convert string values from MySQL to numbers const parsedData: any = { totalAusgaben: parseFloat(data.totalAusgaben) || 0, @@ -77,7 +92,7 @@ export async function GET(request: Request) { return NextResponse.json({ success: true, - data: parsedData, + data: { ...parsedData, katStats }, }); } catch (error) { console.error('Database error:', error); diff --git a/app/page.tsx b/app/page.tsx index d94e351..db074ec 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import AusgabenForm from '@/components/AusgabenForm'; import AusgabenList from '@/components/AusgabenList'; +import MonatsStatistik from '@/components/MonatsStatistik'; import LogoutButton from '@/components/LogoutButton'; import { AusgabenEntry } from '@/types/ausgaben'; import packageJson from '@/package.json'; @@ -12,6 +13,7 @@ export default function Home() { const [entries, setEntries] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedEntry, setSelectedEntry] = useState(null); + const [statsRefreshKey, setStatsRefreshKey] = useState(0); const version = packageJson.version; const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); @@ -43,6 +45,7 @@ export default function Home() { const handleSuccess = () => { setSelectedEntry(null); + setStatsRefreshKey((k) => k + 1); setTimeout(() => { fetchRecentEntries(); }, 100); @@ -50,6 +53,7 @@ export default function Home() { const handleDelete = (id: number) => { setEntries(entries.filter(entry => entry.ID !== id)); + setStatsRefreshKey((k) => k + 1); }; const handleEdit = (entry: AusgabenEntry) => { @@ -93,6 +97,8 @@ export default function Home() {

Eingabe

+ +

Letzte 20 Einträge

{isLoading ? ( diff --git a/components/AusgabenForm.tsx b/components/AusgabenForm.tsx index a1e31ff..b71c2a8 100644 --- a/components/AusgabenForm.tsx +++ b/components/AusgabenForm.tsx @@ -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(null); - // Monthly stats - const [stats, setStats] = useState(null); - const [month, setMonth] = useState(''); - const [year, setYear] = useState(''); - const [isLoadingStats, setIsLoadingStats] = useState(false); - // Autocomplete data const [autoCompleteWo, setAutoCompleteWo] = useState([]); const [autoCompleteWas, setAutoCompleteWas] = useState([]); @@ -41,23 +35,6 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben const [katDropdownOpen, setKatDropdownOpen] = useState(false); const katDropdownRef = useRef(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 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
- - {/* Monatsstatistiken */} -
-
-
- - - - - handleYearChange(e.target.value)} - className="border border-gray-400 rounded px-3 py-1 w-24" - min="2013" - max="2099" - /> -
- -
- {isLoadingStats ? ( - Lade... - ) : stats ? ( - - Summe: {formatAmount(stats.totalAusgaben)} - - ) : null} -
-
-
); diff --git a/components/AusgabenList.tsx b/components/AusgabenList.tsx index 0c71a3f..7532ff1 100644 --- a/components/AusgabenList.tsx +++ b/components/AusgabenList.tsx @@ -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(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 + + + + + )} ); } diff --git a/components/MonatsStatistik.tsx b/components/MonatsStatistik.tsx new file mode 100644 index 0000000..70243d4 --- /dev/null +++ b/components/MonatsStatistik.tsx @@ -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(null); + const [month, setMonth] = useState(''); + const [year, setYear] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [categories, setCategories] = useState([]); + + // 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 ( +
+ {/* Zeile 1: Monat/Jahr + Gesamtsumme */} +
+
+ + + + + setYear(e.target.value)} + className="border border-gray-400 rounded px-3 py-1 w-24" + min="2013" + max="2099" + /> +
+ +
+ {isLoading ? ( + Lade... + ) : stats ? ( + + Summe: {formatAmount(stats.totalAusgaben)} + + ) : null} +
+
+ + {/* Zeile 2+: Kategorien */} + {!isLoading && stats?.katStats && Object.keys(stats.katStats).length > 0 && ( +
+ {Object.entries(stats.katStats).map(([code, total]) => ( +
+ {getCatLabel(code)}: + {formatAmount(total)} +
+ ))} +
+ )} +
+ ); +} diff --git a/types/ausgaben.ts b/types/ausgaben.ts index 1aef7e0..9a34ad5 100644 --- a/types/ausgaben.ts +++ b/types/ausgaben.ts @@ -35,6 +35,7 @@ export interface MonthlyStats { MASTER?: number; Einnahmen: number; Ueberweisungen: number; + katStats?: Record; } // Haushalt Zahlungsarten (TYP = 0)