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 {
|
try {
|
||||||
const { id } = await context.params;
|
const { id } = await context.params;
|
||||||
const body = await request.json();
|
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) {
|
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -23,7 +23,7 @@ export async function PUT(
|
|||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE Ausgaben
|
UPDATE Ausgaben
|
||||||
SET Datum = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, TYP = ?
|
SET Datum = ?, Wo = ?, Was = ?, Kat = ?, Wieviel = ?, Wie = ?, TYP = ?
|
||||||
WHERE ID = ?
|
WHERE ID = ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ export async function PUT(
|
|||||||
Datum,
|
Datum,
|
||||||
Wo,
|
Wo,
|
||||||
Was,
|
Was,
|
||||||
|
Kat || 'L',
|
||||||
parseFloat(Wieviel),
|
parseFloat(Wieviel),
|
||||||
Wie,
|
Wie,
|
||||||
TYP,
|
TYP,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function GET(request: Request) {
|
|||||||
const pool = getDbPool();
|
const pool = getDbPool();
|
||||||
|
|
||||||
let query = `SELECT
|
let query = `SELECT
|
||||||
ID, Datum, Wo, Was, Wieviel, Wie, TYP,
|
ID, Datum, Wo, Was, Kat, Wieviel, Wie, TYP,
|
||||||
CASE DAYOFWEEK(Datum)
|
CASE DAYOFWEEK(Datum)
|
||||||
WHEN 1 THEN 'Sonntag'
|
WHEN 1 THEN 'Sonntag'
|
||||||
WHEN 2 THEN 'Montag'
|
WHEN 2 THEN 'Montag'
|
||||||
@@ -68,7 +68,7 @@ export async function GET(request: Request) {
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
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) {
|
if (!Datum || !Wo || !Was || !Wieviel || !Wie || TYP === undefined) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -80,14 +80,15 @@ export async function POST(request: Request) {
|
|||||||
const pool = getDbPool();
|
const pool = getDbPool();
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO Ausgaben (Datum, Wo, Was, Wieviel, Wie, TYP)
|
INSERT INTO Ausgaben (Datum, Wo, Was, Kat, Wieviel, Wie, TYP)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const [result] = await pool.query<ResultSetHeader>(query, [
|
const [result] = await pool.query<ResultSetHeader>(query, [
|
||||||
Datum,
|
Datum,
|
||||||
Wo,
|
Wo,
|
||||||
Was,
|
Was,
|
||||||
|
Kat || 'L',
|
||||||
parseFloat(Wieviel),
|
parseFloat(Wieviel),
|
||||||
Wie,
|
Wie,
|
||||||
TYP,
|
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';
|
'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 { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN_HAUSHALT, ZAHLUNGSARTEN_PRIVAT, MonthlyStats } from '@/types/ausgaben';
|
||||||
|
import { Category } from '@/app/api/categories/route';
|
||||||
|
|
||||||
interface AusgabenFormProps {
|
interface AusgabenFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@@ -18,6 +19,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
WochTag: '',
|
WochTag: '',
|
||||||
Wo: '',
|
Wo: '',
|
||||||
Was: '',
|
Was: '',
|
||||||
|
Kat: 'L',
|
||||||
Wieviel: '',
|
Wieviel: '',
|
||||||
Wie: defaultZahlungsart,
|
Wie: defaultZahlungsart,
|
||||||
TYP: typ,
|
TYP: typ,
|
||||||
@@ -35,6 +37,9 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
// Autocomplete data
|
// Autocomplete data
|
||||||
const [autoCompleteWo, setAutoCompleteWo] = useState<string[]>([]);
|
const [autoCompleteWo, setAutoCompleteWo] = useState<string[]>([]);
|
||||||
const [autoCompleteWas, setAutoCompleteWas] = 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) => {
|
const fetchStats = useCallback(async (y: string, m: string) => {
|
||||||
if (!y || !m) return;
|
if (!y || !m) return;
|
||||||
@@ -87,6 +92,25 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
fetchAutoComplete();
|
fetchAutoComplete();
|
||||||
}, [typ, 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) => {
|
const handleMonthChange = (newMonth: string) => {
|
||||||
setMonth(newMonth);
|
setMonth(newMonth);
|
||||||
};
|
};
|
||||||
@@ -113,6 +137,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
WochTag: selectedEntry.WochTag,
|
WochTag: selectedEntry.WochTag,
|
||||||
Wo: selectedEntry.Wo,
|
Wo: selectedEntry.Wo,
|
||||||
Was: selectedEntry.Was,
|
Was: selectedEntry.Was,
|
||||||
|
Kat: selectedEntry.Kat || 'L',
|
||||||
Wieviel: selectedEntry.Wieviel.toString(),
|
Wieviel: selectedEntry.Wieviel.toString(),
|
||||||
Wie: selectedEntry.Wie,
|
Wie: selectedEntry.Wie,
|
||||||
TYP: selectedEntry.TYP,
|
TYP: selectedEntry.TYP,
|
||||||
@@ -132,6 +157,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
WochTag: weekday,
|
WochTag: weekday,
|
||||||
Wo: '',
|
Wo: '',
|
||||||
Was: '',
|
Was: '',
|
||||||
|
Kat: 'L',
|
||||||
Wieviel: '',
|
Wieviel: '',
|
||||||
Wie: defaultZahlungsart,
|
Wie: defaultZahlungsart,
|
||||||
TYP: typ,
|
TYP: typ,
|
||||||
@@ -184,6 +210,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
Datum: formData.Datum,
|
Datum: formData.Datum,
|
||||||
Wo: formData.Wo,
|
Wo: formData.Wo,
|
||||||
Was: formData.Was,
|
Was: formData.Was,
|
||||||
|
Kat: formData.Kat,
|
||||||
Wieviel: formData.Wieviel,
|
Wieviel: formData.Wieviel,
|
||||||
Wie: formData.Wie,
|
Wie: formData.Wie,
|
||||||
TYP: formData.TYP,
|
TYP: formData.TYP,
|
||||||
@@ -223,6 +250,7 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
WochTag: weekday,
|
WochTag: weekday,
|
||||||
Wo: '',
|
Wo: '',
|
||||||
Was: '',
|
Was: '',
|
||||||
|
Kat: 'L',
|
||||||
Wieviel: '',
|
Wieviel: '',
|
||||||
Wie: defaultZahlungsart,
|
Wie: defaultZahlungsart,
|
||||||
TYP: typ,
|
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 w-32">Datum</th>
|
||||||
<th className="p-2">Wo</th>
|
<th className="p-2">Wo</th>
|
||||||
<th className="p-2">Was</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-24">Wieviel</th>
|
||||||
<th className="p-2 w-4"></th>
|
<th className="p-2 w-4"></th>
|
||||||
<th className="p-2 w-38 text-left">Wie</th>
|
<th className="p-2 w-38 text-left">Wie</th>
|
||||||
@@ -295,6 +324,35 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
|
|||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
</td>
|
</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">
|
<td className="p-2 w-24">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -29,12 +29,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
const date = new Date(dateStr);
|
return dateStr.toString().split('T')[0];
|
||||||
return date.toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAmount = (amount: number) => {
|
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-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-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-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-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-16">Wie</th>
|
||||||
<th className="border-b-2 border-black p-2 w-38">Aktion</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>
|
<tbody>
|
||||||
{entries.length === 0 ? (
|
{entries.length === 0 ? (
|
||||||
<tr>
|
<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
|
Keine Einträge vorhanden
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 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.Wo}</td>
|
||||||
<td className="border-y border-black p-2">{entry.Was}</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">
|
<td className="border-y border-black p-2 text-right">
|
||||||
{formatAmount(entry.Wieviel)}
|
{formatAmount(entry.Wieviel)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface AusgabenEntry {
|
|||||||
WochTag: string;
|
WochTag: string;
|
||||||
Wo: string;
|
Wo: string;
|
||||||
Was: string;
|
Was: string;
|
||||||
|
Kat: string;
|
||||||
Wieviel: number;
|
Wieviel: number;
|
||||||
Wie: string;
|
Wie: string;
|
||||||
TYP: number;
|
TYP: number;
|
||||||
@@ -16,6 +17,7 @@ export interface CreateAusgabenEntry {
|
|||||||
WochTag: string;
|
WochTag: string;
|
||||||
Wo: string;
|
Wo: string;
|
||||||
Was: string;
|
Was: string;
|
||||||
|
Kat: string;
|
||||||
Wieviel: string | number;
|
Wieviel: string | number;
|
||||||
Wie: string;
|
Wie: string;
|
||||||
TYP: number;
|
TYP: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user