diff --git a/README.md b/README.md index feb5777..d0db749 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,27 @@ Dies ist die modernisierte Version des alten PHP/jQuery-basierten Ausgaben-Progr ## Features -- **Eingabe-Tab**: Erfassen von Ausgaben mit: +- **Zwei Tabs für verschiedene Ausgabenkategorien:** + - **Haushalt (TYP=0)**: Zahlungsarten EC-R, EC-B, bar-R, bar-B, Einnahme, Überweisung + - **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, Master, Einnahme, Überweisung + +- **Eingabe**: Erfassen von Ausgaben mit: - Datum (mit automatischem Wochentag) - Wo (Geschäft/Ort) - Was (Beschreibung) - Wieviel (Betrag in Euro) - - Wie (Zahlungsart: bar, EC, VISA, MASTER, Einnahme, Überweisung) - - Monatsstatistiken - - Letzte 10 Einträge + - Wie (Zahlungsart - abhängig vom aktiven Tab) + - Monatsstatistiken (TYP-spezifisch) + - Letzte 10 Einträge des aktiven TYPs -- **Listen-Tab**: Vollständige Auflistung aller Einträge mit: +- **Listen-Ansicht**: Vollständige Auflistung aller Einträge mit: - Bearbeiten-Funktion - Löschen-Funktion - Sortierung nach Datum (absteigend) + - Filterung nach TYP (Haushalt/Privat) -- **Statistik-Tab**: Monatliche Auswertungen mit: - - Gesamtausgaben +- **Monatliche Statistiken**: + - Gesamtausgaben pro TYP - Aufschlüsselung nach Zahlungsart - Einnahmen - Überweisungen @@ -84,20 +89,22 @@ Die Anwendung verwendet die Tabelle `Ausgaben` mit folgenden Feldern: - `ID` (auto_increment) - `Datum` (date) -- `WochTag` (varchar) - `Wo` (varchar) - Geschäft/Ort - `Was` (varchar) - Beschreibung - `Wieviel` (decimal) - Betrag - `Wie` (varchar) - Zahlungsart +- `TYP` (tinyint) - 0=Haushalt, 1=Privat - `OK` (tinyint) - Kontrollstatus +**Hinweis:** Der Wochentag (`WochTag`) wird nicht in der Datenbank gespeichert, sondern dynamisch aus dem `Datum`-Feld berechnet. + ## API Endpoints -- `GET /api/ausgaben` - Einträge abrufen (mit limit, startDate, month, year params) -- `POST /api/ausgaben` - Neuen Eintrag erstellen -- `PUT /api/ausgaben/[id]` - Eintrag aktualisieren +- `GET /api/ausgaben` - Einträge abrufen (mit limit, startDate, month, year, typ params) +- `POST /api/ausgaben` - Neuen Eintrag erstellen (mit TYP) +- `PUT /api/ausgaben/[id]` - Eintrag aktualisieren (mit TYP) - `DELETE /api/ausgaben/[id]` - Eintrag löschen -- `GET /api/ausgaben/stats` - Monatsstatistiken (mit month, year params) +- `GET /api/ausgaben/stats` - Monatsstatistiken (mit month, year, typ params) ## Migration von der alten Version diff --git a/app/api/ausgaben/[id]/route.ts b/app/api/ausgaben/[id]/route.ts index 1db7f6b..7ff635b 100644 --- a/app/api/ausgaben/[id]/route.ts +++ b/app/api/ausgaben/[id]/route.ts @@ -10,23 +10,23 @@ export async function PUT( try { const { id } = await context.params; const body = await request.json(); - const { Datum, WochTag, Wo, Was, Wieviel, Wie, OK } = body; + const { Datum, Wo, Was, Wieviel, Wie, TYP, OK } = body; const pool = getDbPool(); const query = ` UPDATE Ausgaben - SET Datum = ?, WochTag = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, OK = ? + SET Datum = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, TYP = ?, OK = ? WHERE ID = ? `; const [result] = await pool.query(query, [ Datum, - WochTag, Wo, Was, parseFloat(Wieviel), Wie, + TYP, OK || 0, parseInt(id), ]); diff --git a/app/api/ausgaben/route.ts b/app/api/ausgaben/route.ts index 3e89719..5dfb7bf 100644 --- a/app/api/ausgaben/route.ts +++ b/app/api/ausgaben/route.ts @@ -10,20 +10,41 @@ export async function GET(request: Request) { const startDate = searchParams.get('startDate'); const month = searchParams.get('month'); const year = searchParams.get('year'); + const typ = searchParams.get('typ'); const pool = getDbPool(); - let query = 'SELECT * FROM Ausgaben'; + let query = `SELECT *, + CASE DAYOFWEEK(Datum) + WHEN 1 THEN 'Sonntag' + WHEN 2 THEN 'Montag' + WHEN 3 THEN 'Dienstag' + WHEN 4 THEN 'Mittwoch' + WHEN 5 THEN 'Donnerstag' + WHEN 6 THEN 'Freitag' + WHEN 7 THEN 'Samstag' + END as WochTag + FROM Ausgaben`; const params: any[] = []; + const conditions: string[] = []; + + if (typ !== null && typ !== undefined) { + conditions.push('TYP = ?'); + params.push(parseInt(typ)); + } if (month && year) { - query += ' WHERE YEAR(Datum) = ? AND MONTH(Datum) = ?'; + conditions.push('YEAR(Datum) = ? AND MONTH(Datum) = ?'); params.push(year, month); } else if (startDate) { - query += ' WHERE Datum >= ?'; + conditions.push('Datum >= ?'); params.push(startDate); } + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + query += ' ORDER BY Datum DESC, ID DESC LIMIT ?'; params.push(parseInt(limit)); @@ -46,9 +67,9 @@ export async function GET(request: Request) { export async function POST(request: Request) { try { const body = await request.json(); - const { Datum, WochTag, Wo, Was, Wieviel, Wie, OK } = body; + const { Datum, Wo, Was, Wieviel, Wie, TYP, OK } = body; - if (!Datum || !Wo || !Was || !Wieviel || !Wie) { + if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) { return NextResponse.json( { success: false, error: 'Missing required fields' }, { status: 400 } @@ -58,17 +79,17 @@ export async function POST(request: Request) { const pool = getDbPool(); const query = ` - INSERT INTO Ausgaben (Datum, WochTag, Wo, Was, Wieviel, Wie, OK) + INSERT INTO Ausgaben (Datum, Wo, Was, Wieviel, Wie, TYP, OK) VALUES (?, ?, ?, ?, ?, ?, ?) `; const [result] = await pool.query(query, [ Datum, - WochTag, Wo, Was, parseFloat(Wieviel), Wie, + TYP, OK || 0, ]); diff --git a/app/api/ausgaben/stats/route.ts b/app/api/ausgaben/stats/route.ts index 174ef08..88a06eb 100644 --- a/app/api/ausgaben/stats/route.ts +++ b/app/api/ausgaben/stats/route.ts @@ -8,53 +8,73 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); const month = searchParams.get('month'); const year = searchParams.get('year'); + const typ = searchParams.get('typ'); - if (!month || !year) { + if (!month || !year || typ === null) { return NextResponse.json( - { success: false, error: 'Month and year are required' }, + { success: false, error: 'Month, year and typ are required' }, { status: 400 } ); } const pool = getDbPool(); - // Get total ausgaben and breakdown by payment type - const query = ` - SELECT - SUM(CASE WHEN Wie IN ('EC-R', 'EC-B', 'bar-R', 'bar-B', 'Ueber') THEN Wieviel ELSE 0 END) as totalAusgaben, - SUM(CASE WHEN Wie = 'EC-R' THEN Wieviel ELSE 0 END) as ECR, - SUM(CASE WHEN Wie = 'EC-B' THEN Wieviel ELSE 0 END) as ECB, - SUM(CASE WHEN Wie = 'bar-R' THEN Wieviel ELSE 0 END) as barR, - SUM(CASE WHEN Wie = 'bar-B' THEN Wieviel ELSE 0 END) as barB, - SUM(CASE WHEN Wie = 'Einnahme' THEN Wieviel ELSE 0 END) as Einnahmen, - SUM(CASE WHEN Wie = 'Ueber' THEN Wieviel ELSE 0 END) as Ueberweisungen - FROM Ausgaben - WHERE YEAR(Datum) = ? AND MONTH(Datum) = ? - `; + // Get total ausgaben and breakdown by payment type based on TYP + let query: string; + + if (parseInt(typ) === 0) { + // Haushalt - unterstützt beide Varianten: mit/ohne Bindestrich und Ein/Einnahme, Uber/Ueber + query = ` + SELECT + SUM(CASE WHEN Wie IN ('EC-R', 'ECR', 'EC-B', 'ECB', 'bar-R', 'barR', 'bar-B', 'barB', 'Ueber', 'Uber') THEN Wieviel ELSE 0 END) as totalAusgaben, + SUM(CASE WHEN Wie IN ('EC-R', 'ECR') THEN Wieviel ELSE 0 END) as ECR, + SUM(CASE WHEN Wie IN ('EC-B', 'ECB') THEN Wieviel ELSE 0 END) as ECB, + SUM(CASE WHEN Wie IN ('bar-R', 'barR') THEN Wieviel ELSE 0 END) as barR, + SUM(CASE WHEN Wie IN ('bar-B', 'barB') THEN Wieviel ELSE 0 END) as barB, + SUM(CASE WHEN Wie IN ('Einnahme', 'Ein') THEN Wieviel ELSE 0 END) as Einnahmen, + SUM(CASE WHEN Wie IN ('Ueber', 'Uber') THEN Wieviel ELSE 0 END) as Ueberweisungen + FROM Ausgaben + WHERE YEAR(Datum) = ? AND MONTH(Datum) = ? AND TYP = 0 + `; + } else { + // Privat - unterstützt Uber/Ueber für Überweisung + query = ` + SELECT + SUM(CASE WHEN Wie IN ('bar', 'EC', 'VISA', 'MASTER', 'Uber', 'Ueber') THEN Wieviel ELSE 0 END) as totalAusgaben, + SUM(CASE WHEN Wie = 'bar' THEN Wieviel ELSE 0 END) as bar, + SUM(CASE WHEN Wie = 'EC' THEN Wieviel ELSE 0 END) as EC, + SUM(CASE WHEN Wie = 'VISA' THEN Wieviel ELSE 0 END) as VISA, + SUM(CASE WHEN Wie = 'MASTER' THEN Wieviel ELSE 0 END) as MASTER, + SUM(CASE WHEN Wie = 'Einnahme' THEN Wieviel ELSE 0 END) as Einnahmen, + SUM(CASE WHEN Wie IN ('Uber', 'Ueber') THEN Wieviel ELSE 0 END) as Ueberweisungen + FROM Ausgaben + WHERE YEAR(Datum) = ? AND MONTH(Datum) = ? AND TYP = 1 + `; + } const [rows] = await pool.query(query, [year, month]); - const data = rows[0] || { - totalAusgaben: 0, - ECR: 0, - ECB: 0, - barR: 0, - barB: 0, - Einnahmen: 0, - Ueberweisungen: 0, - }; + const data = rows[0] || {}; // Convert string values from MySQL to numbers - const parsedData = { + const parsedData: any = { totalAusgaben: parseFloat(data.totalAusgaben) || 0, - ECR: parseFloat(data.ECR) || 0, - ECB: parseFloat(data.ECB) || 0, - barR: parseFloat(data.barR) || 0, - barB: parseFloat(data.barB) || 0, Einnahmen: parseFloat(data.Einnahmen) || 0, Ueberweisungen: parseFloat(data.Ueberweisungen) || 0, }; + if (parseInt(typ) === 0) { + parsedData.ECR = parseFloat(data.ECR) || 0; + parsedData.ECB = parseFloat(data.ECB) || 0; + parsedData.barR = parseFloat(data.barR) || 0; + parsedData.barB = parseFloat(data.barB) || 0; + } else { + parsedData.bar = parseFloat(data.bar) || 0; + parsedData.EC = parseFloat(data.EC) || 0; + parsedData.VISA = parseFloat(data.VISA) || 0; + parsedData.MASTER = parseFloat(data.MASTER) || 0; + } + return NextResponse.json({ success: true, data: parsedData, diff --git a/app/page.tsx b/app/page.tsx index d12e21d..c2753ff 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,6 +7,7 @@ import { AusgabenEntry } from '@/types/ausgaben'; import packageJson from '@/package.json'; export default function Home() { + const [activeTab, setActiveTab] = useState(0); // 0 = Haushalt, 1 = Privat const [entries, setEntries] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedEntry, setSelectedEntry] = useState(null); @@ -15,12 +16,13 @@ export default function Home() { useEffect(() => { fetchRecentEntries(); - }, []); + setSelectedEntry(null); // Clear selected entry when switching tabs + }, [activeTab]); const fetchRecentEntries = async () => { setIsLoading(true); try { - const response = await fetch('/api/ausgaben?limit=10', { + const response = await fetch(`/api/ausgaben?limit=10&typ=${activeTab}`, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache', @@ -58,9 +60,33 @@ export default function Home() {

Ausgaben - Log

+ {/* Tab Navigation */} +
+ + +
+

Eingabe

- +

Letzte 10 Einträge

diff --git a/components/AusgabenForm.tsx b/components/AusgabenForm.tsx index d5ea459..87b7410 100644 --- a/components/AusgabenForm.tsx +++ b/components/AusgabenForm.tsx @@ -1,21 +1,26 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN, Zahlungsart, MonthlyStats } from '@/types/ausgaben'; +import { useState, useEffect, useCallback } from 'react'; +import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN_HAUSHALT, ZAHLUNGSARTEN_PRIVAT, MonthlyStats } from '@/types/ausgaben'; interface AusgabenFormProps { onSuccess: () => void; selectedEntry?: AusgabenEntry | null; + typ: number; // 0 = Haushalt, 1 = Privat } -export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormProps) { +export default function AusgabenForm({ onSuccess, selectedEntry, typ }: AusgabenFormProps) { + const zahlungsarten = typ === 0 ? ZAHLUNGSARTEN_HAUSHALT : ZAHLUNGSARTEN_PRIVAT; + const defaultZahlungsart = typ === 0 ? 'ECR' : 'bar'; + const [formData, setFormData] = useState({ Datum: '', WochTag: '', Wo: '', Was: '', Wieviel: '', - Wie: 'EC-R', + Wie: defaultZahlungsart, + TYP: typ, OK: 0, }); @@ -28,22 +33,12 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP const [year, setYear] = useState(''); const [isLoadingStats, setIsLoadingStats] = useState(false); - // Initialize stats with current month/year - useEffect(() => { - const now = new Date(); - const currentMonth = String(now.getMonth() + 1).padStart(2, '0'); - const currentYear = String(now.getFullYear()); - setMonth(currentMonth); - setYear(currentYear); - fetchStats(currentYear, currentMonth); - }, []); - - const fetchStats = async (y: string, m: string) => { + 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}`); + const response = await fetch(`/api/ausgaben/stats?year=${y}&month=${m}&typ=${typ}`); const data = await response.json(); if (data.success) { setStats(data.data); @@ -53,16 +48,30 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP } finally { setIsLoadingStats(false); } - }; + }, [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]); const handleMonthChange = (newMonth: string) => { setMonth(newMonth); - fetchStats(year, newMonth); }; const handleYearChange = (newYear: string) => { setYear(newYear); - fetchStats(newYear, month); }; const formatAmount = (amount: number | null) => { @@ -85,6 +94,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP Was: selectedEntry.Was, Wieviel: selectedEntry.Wieviel.toString(), Wie: selectedEntry.Wie, + TYP: selectedEntry.TYP, OK: selectedEntry.OK || 0, }); @@ -99,11 +109,12 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP ...prev, Datum: dateStr, WochTag: weekday, + TYP: typ, })); setEditId(null); } - }, [selectedEntry]); + }, [selectedEntry, typ]); const getWeekday = (date: Date): string => { const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; @@ -167,7 +178,8 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP Wo: '', Was: '', Wieviel: '', - Wie: 'EC-R', + Wie: defaultZahlungsart, + TYP: typ, OK: 0, }); @@ -240,11 +252,11 @@ export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormP