Kategorien dazu
This commit is contained in:
5
add_kategorie.sql
Normal file
5
add_kategorie.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Migration: Kategorie-Spalte zur Ausgaben-Tabelle hinzufügen
|
||||
-- Ausführen: mysql -u <user> -p <database> < add_kategorie.sql
|
||||
|
||||
ALTER TABLE Ausgaben
|
||||
ADD COLUMN Kat VARCHAR(4) NOT NULL DEFAULT 'L' AFTER Was;
|
||||
@@ -10,7 +10,7 @@ export async function PUT(
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const body = await request.json();
|
||||
const { Datum, Wo, Was, Wieviel, Wie, TYP } = body;
|
||||
const { Datum, Wo, Was, Kat, Wieviel, Wie, TYP } = body;
|
||||
|
||||
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
|
||||
return NextResponse.json(
|
||||
@@ -23,7 +23,7 @@ export async function PUT(
|
||||
|
||||
const query = `
|
||||
UPDATE Ausgaben
|
||||
SET Datum = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, TYP = ?
|
||||
SET Datum = ?, Wo = ?, Was = ?, Kat = ?, Wieviel = ?, Wie = ?, TYP = ?
|
||||
WHERE ID = ?
|
||||
`;
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function PUT(
|
||||
Datum,
|
||||
Wo,
|
||||
Was,
|
||||
Kat || 'L',
|
||||
parseFloat(Wieviel),
|
||||
Wie,
|
||||
TYP,
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function GET(request: Request) {
|
||||
const pool = getDbPool();
|
||||
|
||||
let query = `SELECT
|
||||
ID, Datum, Wo, Was, Wieviel, Wie, TYP,
|
||||
ID, Datum, Wo, Was, Kat, Wieviel, Wie, TYP,
|
||||
CASE DAYOFWEEK(Datum)
|
||||
WHEN 1 THEN 'Sonntag'
|
||||
WHEN 2 THEN 'Montag'
|
||||
@@ -68,7 +68,7 @@ export async function GET(request: Request) {
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { Datum, Wo, Was, Wieviel, Wie, TYP } = body;
|
||||
const { Datum, Wo, Was, Kat, Wieviel, Wie, TYP } = body;
|
||||
|
||||
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
|
||||
return NextResponse.json(
|
||||
@@ -80,14 +80,15 @@ export async function POST(request: Request) {
|
||||
const pool = getDbPool();
|
||||
|
||||
const query = `
|
||||
INSERT INTO Ausgaben (Datum, Wo, Was, Wieviel, Wie, TYP)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO Ausgaben (Datum, Wo, Was, Kat, Wieviel, Wie, TYP)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const [result] = await pool.query<ResultSetHeader>(query, [
|
||||
Datum,
|
||||
Wo,
|
||||
Was,
|
||||
Kat || 'L',
|
||||
parseFloat(Wieviel),
|
||||
Wie,
|
||||
TYP,
|
||||
|
||||
33
app/api/categories/route.ts
Normal file
33
app/api/categories/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface Category {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// GET /api/categories - Fetch categories from categories.txt
|
||||
export async function GET() {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'categories.txt');
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
const categories: Category[] = content
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.includes('='))
|
||||
.map((line) => {
|
||||
const [value, label] = line.split('=');
|
||||
return { value: value.trim(), label: label.trim() };
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, data: categories });
|
||||
} catch (error) {
|
||||
console.error('Error reading categories:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Could not load categories' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
15
categories.txt
Normal file
15
categories.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
R=Restaurant
|
||||
L=Lebensmittel
|
||||
H=Haushalt
|
||||
Ku=Kultur
|
||||
Kl=Kleidung
|
||||
Dr=Drogerie
|
||||
Ap=Apotheke
|
||||
Ar=Arzt
|
||||
Re=Reise
|
||||
Au=Auto
|
||||
El=Elektronik
|
||||
Fr=Freizeit
|
||||
Ge=Getränke
|
||||
Ba=Bäckerei
|
||||
So=Sonstiges
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN_HAUSHALT, ZAHLUNGSARTEN_PRIVAT, MonthlyStats } from '@/types/ausgaben';
|
||||
import { Category } from '@/app/api/categories/route';
|
||||
|
||||
interface AusgabenFormProps {
|
||||
onSuccess: () => void;
|
||||
@@ -18,6 +19,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
WochTag: '',
|
||||
Wo: '',
|
||||
Was: '',
|
||||
Kat: 'L',
|
||||
Wieviel: '',
|
||||
Wie: defaultZahlungsart,
|
||||
TYP: typ,
|
||||
@@ -35,6 +37,9 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
// Autocomplete data
|
||||
const [autoCompleteWo, setAutoCompleteWo] = useState<string[]>([]);
|
||||
const [autoCompleteWas, setAutoCompleteWas] = useState<string[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [katDropdownOpen, setKatDropdownOpen] = useState(false);
|
||||
const katDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchStats = useCallback(async (y: string, m: string) => {
|
||||
if (!y || !m) return;
|
||||
@@ -87,6 +92,25 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
fetchAutoComplete();
|
||||
}, [typ, fetchAutoComplete]);
|
||||
|
||||
// Close Kat dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (katDropdownRef.current && !katDropdownRef.current.contains(e.target as Node)) {
|
||||
setKatDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Fetch categories once on mount
|
||||
useEffect(() => {
|
||||
fetch('/api/categories')
|
||||
.then((r) => r.json())
|
||||
.then((data) => { if (data.success) setCategories(data.data); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleMonthChange = (newMonth: string) => {
|
||||
setMonth(newMonth);
|
||||
};
|
||||
@@ -113,6 +137,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
WochTag: selectedEntry.WochTag,
|
||||
Wo: selectedEntry.Wo,
|
||||
Was: selectedEntry.Was,
|
||||
Kat: selectedEntry.Kat || 'L',
|
||||
Wieviel: selectedEntry.Wieviel.toString(),
|
||||
Wie: selectedEntry.Wie,
|
||||
TYP: selectedEntry.TYP,
|
||||
@@ -132,6 +157,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
WochTag: weekday,
|
||||
Wo: '',
|
||||
Was: '',
|
||||
Kat: 'L',
|
||||
Wieviel: '',
|
||||
Wie: defaultZahlungsart,
|
||||
TYP: typ,
|
||||
@@ -184,6 +210,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
Datum: formData.Datum,
|
||||
Wo: formData.Wo,
|
||||
Was: formData.Was,
|
||||
Kat: formData.Kat,
|
||||
Wieviel: formData.Wieviel,
|
||||
Wie: formData.Wie,
|
||||
TYP: formData.TYP,
|
||||
@@ -223,6 +250,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
WochTag: weekday,
|
||||
Wo: '',
|
||||
Was: '',
|
||||
Kat: 'L',
|
||||
Wieviel: '',
|
||||
Wie: defaultZahlungsart,
|
||||
TYP: typ,
|
||||
@@ -247,6 +275,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
<th className="p-2 w-32">Datum</th>
|
||||
<th className="p-2">Wo</th>
|
||||
<th className="p-2">Was</th>
|
||||
<th className="p-2 w-12">Kat.</th>
|
||||
<th className="p-2 w-24">Wieviel</th>
|
||||
<th className="p-2 w-4"></th>
|
||||
<th className="p-2 w-38 text-left">Wie</th>
|
||||
@@ -295,6 +324,35 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
||||
))}
|
||||
</datalist>
|
||||
</td>
|
||||
<td className="p-2 w-12">
|
||||
<div ref={katDropdownRef} className="relative w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setKatDropdownOpen((o) => !o)}
|
||||
className="w-full px-2 py-1 text-base text-left rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
{formData.Kat}
|
||||
</button>
|
||||
{katDropdownOpen && (
|
||||
<ul className="absolute z-50 left-0 mt-1 w-48 bg-white border-2 border-gray-400 rounded shadow-lg max-h-60 overflow-y-auto text-left">
|
||||
{categories.map((cat) => (
|
||||
<li
|
||||
key={cat.value}
|
||||
className={`px-3 py-1 cursor-pointer hover:bg-blue-100 text-sm ${
|
||||
formData.Kat === cat.value ? 'bg-blue-50 font-semibold' : ''
|
||||
}`}
|
||||
onMouseDown={() => {
|
||||
setFormData({ ...formData, Kat: cat.value });
|
||||
setKatDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{cat.value} - {cat.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 w-24">
|
||||
<input
|
||||
type="number"
|
||||
|
||||
@@ -29,12 +29,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
return dateStr.toString().split('T')[0];
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
@@ -53,6 +48,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
<th className="border-b-2 border-black p-2 w-12">Tag</th>
|
||||
<th className="border-b-2 border-black p-2 w-36">Wo</th>
|
||||
<th className="border-b-2 border-black p-2 w-48">Was</th>
|
||||
<th className="border-b-2 border-black p-2 w-12">Kat.</th>
|
||||
<th className="border-b-2 border-black p-2 w-8">Betrag</th>
|
||||
<th className="border-b-2 border-black p-2 w-16">Wie</th>
|
||||
<th className="border-b-2 border-black p-2 w-38">Aktion</th>
|
||||
@@ -61,7 +57,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
<tbody>
|
||||
{entries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center p-4 text-gray-500">
|
||||
<td colSpan={8} className="text-center p-4 text-gray-500">
|
||||
Keine Einträge vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,6 +70,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
||||
<td className="border-y border-black p-2 text-center">{entry.WochTag.slice(0, 2)}</td>
|
||||
<td className="border-y border-black p-2">{entry.Wo}</td>
|
||||
<td className="border-y border-black p-2">{entry.Was}</td>
|
||||
<td className="border-y border-black p-2 text-center">{entry.Kat}</td>
|
||||
<td className="border-y border-black p-2 text-right">
|
||||
{formatAmount(entry.Wieviel)}
|
||||
</td>
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AusgabenEntry {
|
||||
WochTag: string;
|
||||
Wo: string;
|
||||
Was: string;
|
||||
Kat: string;
|
||||
Wieviel: number;
|
||||
Wie: string;
|
||||
TYP: number;
|
||||
@@ -16,6 +17,7 @@ export interface CreateAusgabenEntry {
|
||||
WochTag: string;
|
||||
Wo: string;
|
||||
Was: string;
|
||||
Kat: string;
|
||||
Wieviel: string | number;
|
||||
Wie: string;
|
||||
TYP: number;
|
||||
|
||||
Reference in New Issue
Block a user