This commit is contained in:
2026-02-27 16:14:40 +00:00
parent b44d5689bb
commit 14bb3fd2cd
5 changed files with 119 additions and 96 deletions

View File

@@ -9,29 +9,19 @@ Dies ist die modernisierte Version des alten PHP/jQuery-basierten Ausgaben-Progr
## Features
- **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
- **Haushalt (TYP=0)**: Zahlungsarten ECR, ECB, barR, barB, Ein(nahme), Uber(weisung)
- **Privat (TYP=1)**: Zahlungsarten bar, EC, VISA, MASTER, Einnahme, Uber(weisung)
- **Eingabe**: Erfassen von Ausgaben mit:
- **Eingabeformular mit integrierten Features:**
- Datum (mit automatischem Wochentag)
- Wo (Geschäft/Ort)
- Was (Beschreibung)
- Wieviel (Betrag in Euro)
- Wie (Zahlungsart - abhängig vom aktiven Tab)
- Monatsstatistiken (TYP-spezifisch)
- Letzte 10 Einträge des aktiven TYPs
- **Listen-Ansicht**: Vollständige Auflistung aller Einträge mit:
- Bearbeiten-Funktion
- Löschen-Funktion
- Sortierung nach Datum (absteigend)
- Filterung nach TYP (Haushalt/Privat)
- **Monatliche Statistiken**:
- Gesamtausgaben pro TYP
- Aufschlüsselung nach Zahlungsart
- Einnahmen
- Überweisungen
- Monatliche Statistiken im Formular (Gesamtsumme, aufgeschlüsselt nach Zahlungsart)
- Letzte 10 Einträge direkt unter dem Formular mit Bearbeiten/Löschen-Funktion
- Bearbeiten-Funktion: Klick auf Eintrag lädt ihn ins Formular
- Filterung nach aktivem TYP (Haushalt/Privat)
## Technologie-Stack

View File

@@ -13,6 +13,7 @@ export default function Home() {
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
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' });
useEffect(() => {
fetchRecentEntries();
@@ -22,7 +23,7 @@ export default function Home() {
const fetchRecentEntries = async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/ausgaben?limit=10&typ=${activeTab}`, {
const response = await fetch(`/api/ausgaben?limit=20&typ=${activeTab}`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
@@ -89,7 +90,7 @@ export default function Home() {
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} typ={activeTab} />
<div className="mt-6 bg-white border border-black rounded-lg shadow-md p-6">
<h3 className="text-xl font-semibold mb-4">Letzte 10 Einträge</h3>
<h3 className="text-xl font-semibold mb-4">Letzte 20 Einträge</h3>
{isLoading ? (
<div className="text-center py-4">Lade Daten...</div>
) : (
@@ -106,7 +107,7 @@ export default function Home() {
</a>
</div>
<div className="text-right">
Version {version}
Version {version} - {buildDate}
</div>
</footer>
</main>

View File

@@ -264,83 +264,78 @@ export default function AusgabenForm({ onSuccess, selectedEntry, typ }: Ausgaben
</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>
{/* Wochentag */}
<div className="mt-3 text-left pl-3">
<span className="font-semibold">{formData.WochTag}</span>
</div>
{/* Buttons */}
<div className="mb-3 flex gap-10 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>
{/* Monatsstatistiken */}
<div className="mt-6 pt-4 pb-6 -mb-6 border-t border-black -mx-6 px-6 bg-[#E0E0FF]">
<div className="flex items-center justify-between pt-1">
<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>
</div>
</form>
</div>
);

View File

@@ -67,7 +67,7 @@ export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenList
</tr>
) : (
entries.map((entry, index) => (
<tr key={entry.ID} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-100'}>
<tr key={entry.ID || `entry-idx-${index}`} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-100'}>
<td className="border-y border-black p-2 text-center">
{formatDate(entry.Datum)}
</td>

37
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,37 @@
# Docker Compose für Production Server mit Traefik
services:
ausgaben-app:
image: docker.citysensor.de/ausgaben-next:latest
container_name: ausgaben-next-app
restart: unless-stopped
expose:
- 3000
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_NAME=${DB_NAME}
labels:
- traefik.enable=true
- traefik.http.routers.ausgaben.entrypoints=http
- traefik.http.routers.ausgaben.rule=Host(`ausgaben.fuerst-stuttgart.de`)
- traefik.http.middlewares.ausgaben-https-redirect.redirectscheme.scheme=https
- traefik.http.routers.ausgaben.middlewares=ausgaben-https-redirect
- traefik.http.routers.ausgaben-secure.entrypoints=https
- traefik.http.routers.ausgaben-secure.rule=Host(`ausgaben.fuerst-stuttgart.de`)
- traefik.http.routers.ausgaben-secure.tls=true
- traefik.http.routers.ausgaben-secure.tls.certresolver=letsencrypt
- traefik.http.routers.ausgaben-secure.service=ausgaben
- traefik.http.services.ausgaben.loadbalancer.server.port=3000
networks:
- proxy
- gitea-internal
networks:
proxy:
name: dockge_default
external: true
gitea-internal:
name: gitea_gitea-internal
external: true