348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
'use client';
|
||
|
||
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, typ }: AusgabenFormProps) {
|
||
const zahlungsarten = typ === 0 ? ZAHLUNGSARTEN_HAUSHALT : ZAHLUNGSARTEN_PRIVAT;
|
||
const defaultZahlungsart = typ === 0 ? 'ECR' : 'bar';
|
||
|
||
const [formData, setFormData] = useState<CreateAusgabenEntry>({
|
||
Datum: '',
|
||
WochTag: '',
|
||
Wo: '',
|
||
Was: '',
|
||
Wieviel: '',
|
||
Wie: defaultZahlungsart,
|
||
TYP: typ,
|
||
OK: 0,
|
||
});
|
||
|
||
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);
|
||
|
||
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]);
|
||
|
||
// 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);
|
||
};
|
||
|
||
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) {
|
||
// Load selected entry for editing
|
||
const dateStr = selectedEntry.Datum.toString().split('T')[0];
|
||
|
||
setFormData({
|
||
Datum: dateStr,
|
||
WochTag: selectedEntry.WochTag,
|
||
Wo: selectedEntry.Wo,
|
||
Was: selectedEntry.Was,
|
||
Wieviel: selectedEntry.Wieviel.toString(),
|
||
Wie: selectedEntry.Wie,
|
||
TYP: selectedEntry.TYP,
|
||
OK: selectedEntry.OK || 0,
|
||
});
|
||
|
||
setEditId(selectedEntry.ID);
|
||
} else {
|
||
// Initialize with current date for new entry
|
||
const now = new Date();
|
||
const dateStr = now.toISOString().split('T')[0];
|
||
const weekday = getWeekday(now);
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
Datum: dateStr,
|
||
WochTag: weekday,
|
||
TYP: typ,
|
||
}));
|
||
|
||
setEditId(null);
|
||
}
|
||
}, [selectedEntry, typ]);
|
||
|
||
const getWeekday = (date: Date): string => {
|
||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||
return weekdays[date.getDay()];
|
||
};
|
||
|
||
const handleDateChange = (dateStr: string) => {
|
||
const [year, month, day] = dateStr.split('-');
|
||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||
const weekday = getWeekday(date);
|
||
|
||
setFormData(prev => ({ ...prev, Datum: dateStr, WochTag: weekday }));
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!formData.Wo || !formData.Was || !formData.Wieviel) {
|
||
alert('Bitte alle Pflichtfelder ausfüllen!');
|
||
return;
|
||
}
|
||
|
||
setIsSubmitting(true);
|
||
|
||
try {
|
||
const url = editId ? `/api/ausgaben/${editId}` : '/api/ausgaben';
|
||
const method = editId ? 'PUT' : 'POST';
|
||
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(formData),
|
||
});
|
||
|
||
if (response.ok) {
|
||
handleReset();
|
||
onSuccess();
|
||
// Refresh stats after successful save
|
||
fetchStats(year, month);
|
||
} else {
|
||
alert('Fehler beim Speichern!');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('Fehler beim Speichern!');
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handleReset = () => {
|
||
const now = new Date();
|
||
const dateStr = now.toISOString().split('T')[0];
|
||
const weekday = getWeekday(now);
|
||
|
||
setFormData({
|
||
Datum: dateStr,
|
||
WochTag: weekday,
|
||
Wo: '',
|
||
Was: '',
|
||
Wieviel: '',
|
||
Wie: defaultZahlungsart,
|
||
TYP: typ,
|
||
OK: 0,
|
||
});
|
||
|
||
setEditId(null);
|
||
};
|
||
|
||
return (
|
||
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6">
|
||
{editId && (
|
||
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
|
||
ℹ️ <strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag.
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={handleSubmit}>
|
||
<table className="w-full text-center">
|
||
<thead>
|
||
<tr >
|
||
<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-24">Wieviel</th>
|
||
<th className="p-2 w-4"></th>
|
||
<th className="p-2 w-38 text-left">Wie</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td className="p-2 w-32">
|
||
<input
|
||
type="date"
|
||
value={formData.Datum}
|
||
onChange={(e) => handleDateChange(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"
|
||
required
|
||
/>
|
||
</td>
|
||
<td className="p-2">
|
||
<input
|
||
type="text"
|
||
value={formData.Wo}
|
||
onChange={(e) => setFormData({ ...formData, Wo: 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"
|
||
placeholder="Geschäft/Ort"
|
||
required
|
||
/>
|
||
</td>
|
||
<td className="p-2">
|
||
<input
|
||
type="text"
|
||
value={formData.Was}
|
||
onChange={(e) => setFormData({ ...formData, Was: 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"
|
||
placeholder="Beschreibung"
|
||
required
|
||
/>
|
||
</td>
|
||
<td className="p-2 w-24">
|
||
<input
|
||
type="number"
|
||
step="0.01"
|
||
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"
|
||
placeholder="0.00"
|
||
required
|
||
/>
|
||
</td>
|
||
<td className="p-2 w-4 text-left">€</td>
|
||
<td className="p-2 w-38">
|
||
<select
|
||
value={formData.Wie}
|
||
onChange={(e) => setFormData({ ...formData, Wie: 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"
|
||
required
|
||
>
|
||
{zahlungsarten.map((za) => (
|
||
<option key={za.value} value={za.value}>
|
||
{za.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th className="p-2 w-32">{formData.WochTag}</th>
|
||
<th className="p-2"></th>
|
||
<th className="p-2"></th>
|
||
<th className="p-2"></th>
|
||
<th className="p-2"></th>
|
||
<th className="p-2"></th>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={6} className="p-3">
|
||
<div className="flex gap-3 justify-center">
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors disabled:opacity-50"
|
||
>
|
||
{isSubmitting ? 'Speichere...' : editId ? 'Aktualisieren' : 'Speichern'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleReset}
|
||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
|
||
>
|
||
Löschen
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={6} className="p-3 pt-6 border-t border-black">
|
||
<div className="flex items-center justify-between">
|
||
<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>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|