First commit - es tut schon mal ganz gut
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.example
|
||||
.env*.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=RXF
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
193
DEPLOYMENT.md
Normal file
193
DEPLOYMENT.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Deployment Guide - Ausgaben-Next
|
||||
|
||||
## Lokale Entwicklung
|
||||
|
||||
### Voraussetzungen
|
||||
- Node.js 22+
|
||||
- MySQL 8+ (Docker container läuft bereits)
|
||||
- npm oder yarn
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Dependencies installieren:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Umgebungsvariablen konfigurieren:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Bearbeiten Sie .env mit Ihren Datenbankzugangsdaten
|
||||
```
|
||||
|
||||
3. **Development Server starten:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter http://localhost:3000 erreichbar.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Mit Docker (Empfohlen)
|
||||
|
||||
1. **Lokaler Build:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml build
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
```
|
||||
|
||||
Die Anwendung läuft dann auf Port 3001 (konfigurierbar in docker-compose.local.yml).
|
||||
|
||||
2. **Logs prüfen:**
|
||||
```bash
|
||||
docker logs ausgaben-next-app
|
||||
```
|
||||
|
||||
### Ohne Docker
|
||||
|
||||
1. **Production Build:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Production Server starten:**
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Datenbank
|
||||
|
||||
### Schema erstellen
|
||||
|
||||
Falls die Tabelle `Ausgaben_Tag` noch nicht existiert:
|
||||
|
||||
```bash
|
||||
mysql -u root -p RXF < create_table.sql
|
||||
```
|
||||
|
||||
### Datenbank-Verbindung
|
||||
|
||||
Die Anwendung verwendet die gleiche MySQL-Datenbank wie die alte Ausgaben-Anwendung:
|
||||
|
||||
- **Host:** gitea-db (oder localhost für lokale Entwicklung)
|
||||
- **Database:** RXF
|
||||
- **Table:** Ausgaben_Tag
|
||||
|
||||
Die Zugangsdaten werden über Umgebungsvariablen in `.env` konfiguriert.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
Erforderliche Variablen in `.env`:
|
||||
|
||||
```env
|
||||
DB_HOST=gitea-db # oder localhost für lokale Entwicklung
|
||||
DB_USER=root
|
||||
DB_PASS=Ihr_Passwort
|
||||
DB_NAME=RXF
|
||||
```
|
||||
|
||||
## Port-Konfiguration
|
||||
|
||||
- **Development:** Port 3000 (npm run dev)
|
||||
- **Production (lokal):** Port 3000 (npm start)
|
||||
- **Docker (lokal):** Port 3001 (gemappt auf internen Port 3000)
|
||||
|
||||
Ändern Sie den Port in `docker-compose.local.yml` falls nötig:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "0.0.0.0:DEIN_PORT:3000"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Datenbankverbindung schlägt fehl
|
||||
|
||||
1. Prüfen Sie die Umgebungsvariablen in `.env`
|
||||
2. Stellen Sie sicher, dass der MySQL-Container läuft:
|
||||
```bash
|
||||
docker ps | grep mysql
|
||||
```
|
||||
3. Falls Docker verwendet wird, prüfen Sie ob die Container im gleichen Netzwerk sind
|
||||
|
||||
### Build-Fehler
|
||||
|
||||
1. Löschen Sie `.next` und `node_modules`:
|
||||
```bash
|
||||
rm -rf .next node_modules
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Prüfen Sie die Node.js Version:
|
||||
```bash
|
||||
node --version # Sollte >= 22.x sein
|
||||
```
|
||||
|
||||
### Port bereits belegt
|
||||
|
||||
Ändern Sie den Port in `docker-compose.local.yml` oder verwenden Sie einen anderen Port:
|
||||
```bash
|
||||
PORT=3002 npm run dev
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Logs ansehen
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker logs -f ausgaben-next-app
|
||||
```
|
||||
|
||||
**Ohne Docker:**
|
||||
Logs werden in der Konsole angezeigt wo `npm start` läuft.
|
||||
|
||||
## Updates
|
||||
|
||||
1. **Code aktualisieren:**
|
||||
```bash
|
||||
git pull # falls Git verwendet wird
|
||||
```
|
||||
|
||||
2. **Dependencies aktualisieren:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Neu bauen und starten:**
|
||||
```bash
|
||||
npm run build
|
||||
# Dann entweder:
|
||||
npm start
|
||||
# oder:
|
||||
docker-compose -f docker-compose.local.yml up -d --build
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Datenbank
|
||||
|
||||
Die Datenbank sollte regelmäßig gesichert werden:
|
||||
|
||||
```bash
|
||||
mysqldump -u root -p RXF Ausgaben_Tag > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Application Files
|
||||
|
||||
Wichtige Dateien für Backup:
|
||||
- `.env` (Konfiguration)
|
||||
- `public/` (falls custom Dateien hinzugefügt wurden)
|
||||
- Die Datenbank (siehe oben)
|
||||
|
||||
## Security
|
||||
|
||||
1. **Niemals `.env` ins Git-Repository einchecken!**
|
||||
2. Verwenden Sie starke Datenbankpasswörter
|
||||
3. Halten Sie Dependencies aktuell:
|
||||
```bash
|
||||
npm audit
|
||||
npm update
|
||||
```
|
||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
# Multi-stage build for Next.js application
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Set build date as build argument
|
||||
ARG BUILD_DATE
|
||||
ENV NEXT_PUBLIC_BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
# Disable telemetry during build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
219
MIGRATION.md
Normal file
219
MIGRATION.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Migration Guide: Von Ausgaben (PHP/jQuery) zu Ausgaben-Next
|
||||
|
||||
Dieser Leitfaden hilft Ihnen bei der Migration von der alten PHP/jQuery-basierten Ausgaben-Anwendung zur neuen Next.js-Version.
|
||||
|
||||
## Übersicht
|
||||
|
||||
### Alte Version (`../Ausgaben`)
|
||||
- **Frontend:** HTML + jQuery + jqGrid
|
||||
- **Backend:** PHP
|
||||
- **Styling:** Custom CSS
|
||||
- **Datums-Handling:** jQuery UI Datepicker
|
||||
- **Tab-System:** jQuery UI Tabs
|
||||
|
||||
### Neue Version (`ausgaben-next`)
|
||||
- **Frontend:** React 19 + TypeScript
|
||||
- **Backend:** Next.js API Routes
|
||||
- **Styling:** Tailwind CSS v4
|
||||
- **Datums-Handling:** Native HTML5 Date Input
|
||||
- **Tab-System:** React State Management
|
||||
|
||||
## Was bleibt gleich?
|
||||
|
||||
✓ **Datenbank:** Gleiche MySQL-Datenbank (`RXF.Ausgaben_Tag`)
|
||||
✓ **Datenstruktur:** Identische Tabellenfelder
|
||||
✓ **Funktionalität:** Alle Features der alten Version
|
||||
✓ **Look & Feel:** Ähnliches Design (angelehnt an werte-next)
|
||||
|
||||
## Feature-Vergleich
|
||||
|
||||
| Feature | Alte Version | Neue Version | Status |
|
||||
|---------|-------------|--------------|---------|
|
||||
| Eingabe-Formular | ✓ jQuery | ✓ React | ✓ Implementiert |
|
||||
| Letzte 10 Einträge | ✓ jqGrid | ✓ React Table | ✓ Implementiert |
|
||||
| Liste aller Einträge | ✓ jqGrid | ✓ React Table | ✓ Implementiert |
|
||||
| Monats-Statistiken | ✓ PHP + AJAX | ✓ API Routes | ✓ Implementiert |
|
||||
| Bearbeiten | ✓ | ✓ | ✓ Implementiert |
|
||||
| Löschen | ✓ | ✓ | ✓ Implementiert |
|
||||
| Datepicker | ✓ jQuery UI | ✓ HTML5 | ✓ Implementiert |
|
||||
| Wochentag auto | ✓ | ✓ | ✓ Implementiert |
|
||||
| OK-Checkbox | ✓ | ✓ | ✓ Implementiert |
|
||||
| Tab-Navigation | ✓ jQuery UI | ✓ React | ✓ Implementiert |
|
||||
|
||||
## Datenbank
|
||||
|
||||
Die Datenbanktabelle bleibt **unverändert**:
|
||||
|
||||
```sql
|
||||
Ausgaben_Tag (
|
||||
ID int(11) AUTO_INCREMENT,
|
||||
Datum date,
|
||||
WochTag varchar(20),
|
||||
Wo varchar(255),
|
||||
Was varchar(500),
|
||||
Wieviel decimal(10,2),
|
||||
Wie varchar(50),
|
||||
OK tinyint(1)
|
||||
)
|
||||
```
|
||||
|
||||
### Wichtig:
|
||||
- Beide Anwendungen können parallel auf **derselben Datenbank** laufen!
|
||||
- Keine Datenmigration notwendig
|
||||
- Bestehende Daten werden automatisch übernommen
|
||||
|
||||
## Parallelbetrieb
|
||||
|
||||
Sie können beide Versionen gleichzeitig laufen lassen:
|
||||
|
||||
1. **Alte Version:** Bleibt unter ihrem bisherigen Pfad aktiv
|
||||
2. **Neue Version:** Läuft auf eigenem Port (z.B. 3001)
|
||||
3. **Datenbank:** Wird von beiden genutzt
|
||||
|
||||
### Vorteile:
|
||||
- Sanfte Migration ohne Downtime
|
||||
- Training mit neuer Version möglich
|
||||
- Fallback zur alten Version jederzeit möglich
|
||||
- Schrittweise Umstellung der Nutzer
|
||||
|
||||
## Unterschiede im Verhalten
|
||||
|
||||
### 1. Datepicker
|
||||
**Alt:** jQuery UI Datepicker mit Pop-up
|
||||
**Neu:** Native HTML5 Date Input (Browser-native Erscheinung)
|
||||
|
||||
### 2. Auto-Complete
|
||||
**Alt:** jQuery Autocomplete für "Wo" und "Was" Felder
|
||||
**Neu:** Noch nicht implementiert (kann später hinzugefügt werden)
|
||||
|
||||
### 3. Tab-Umschaltung
|
||||
**Alt:** Seite bleibt identisch, nur Tabs wechseln
|
||||
**Neu:** React State Management, kein Page Reload
|
||||
|
||||
### 4. Datums-Formatierung
|
||||
**Alt:** Server-seitig (PHP) formatiert
|
||||
**Neu:** Client-seitig (JavaScript Intl API)
|
||||
|
||||
## Migrationsschritte
|
||||
|
||||
### Schritt 1: Installation
|
||||
```bash
|
||||
cd /home/rxf/Projekte/ausgaben-next
|
||||
npm install
|
||||
```
|
||||
|
||||
### Schritt 2: Umgebungsvariablen
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Bearbeiten Sie .env und verwenden Sie die gleichen
|
||||
# Datenbankzugangsdaten wie die alte Version
|
||||
```
|
||||
|
||||
### Schritt 3: Test
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Öffnen Sie http://localhost:3000 und testen Sie die Funktionalität.
|
||||
|
||||
### Schritt 4: Production Build
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Schritt 5: Docker Deployment (Optional)
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
```
|
||||
|
||||
### Schritt 6: Parallelbetrieb testen
|
||||
- Alte Version: Weiter unter bisheriger URL
|
||||
- Neue Version: Unter Port 3001 (oder konfiguriert)
|
||||
- Beide greifen auf gleiche Datenbank zu
|
||||
|
||||
### Schritt 7: Umstellung
|
||||
Wenn Sie mit der neuen Version zufrieden sind:
|
||||
1. Ändern Sie die Ports/URLs in Ihrer Infrastruktur
|
||||
2. Leiten Sie Traffic zur neuen Version um
|
||||
3. Behalten Sie alte Version als Backup
|
||||
|
||||
## Was wurde verbessert?
|
||||
|
||||
### Performance
|
||||
- ✓ Schnelleres Loading durch Server-Side Rendering
|
||||
- ✓ Optimierte Bundles durch Webpack/Turbopack
|
||||
- ✓ Lazy Loading von Komponenten
|
||||
|
||||
### Entwicklung
|
||||
- ✓ TypeScript für bessere Code-Qualität
|
||||
- ✓ Moderne React Patterns (Hooks)
|
||||
- ✓ Wiederverwendbare Komponenten
|
||||
|
||||
### Wartbarkeit
|
||||
- ✓ Klare Trennung von Logik und Präsentation
|
||||
- ✓ Testbare Komponenten
|
||||
- ✓ Moderne Toolchain
|
||||
|
||||
### Security
|
||||
- ✓ Aktuellere Dependencies
|
||||
- ✓ Keine direct SQL-Queries im Frontend
|
||||
- ✓ Input Validation
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
1. **Auto-Complete:** Noch nicht implementiert für "Wo" und "Was" Felder
|
||||
2. **Drucken:** Print-Funktionalität noch nicht implementiert
|
||||
3. **Anleitung-Seite:** Noch nicht erstellt (kann kopiert werden)
|
||||
|
||||
Diese Features können bei Bedarf hinzugefügt werden.
|
||||
|
||||
## Rollback-Plan
|
||||
|
||||
Falls Sie zur alten Version zurück müssen:
|
||||
|
||||
1. Stoppen Sie den Next.js Server/Container
|
||||
2. Die alte PHP-Version funktioniert weiterhin
|
||||
3. Keine Datenbank-Änderungen notwendig
|
||||
|
||||
## Support und Wartung
|
||||
|
||||
### Alte Version
|
||||
- PHP 7.4+ erforderlich
|
||||
- jQuery und jQuery UI müssen geladen bleiben
|
||||
- Keine aktive Weiterentwicklung geplant
|
||||
|
||||
### Neue Version
|
||||
- Node.js 22+
|
||||
- Regelmäßige Dependency-Updates empfohlen
|
||||
- Moderne Stack ermöglicht einfache Erweiterungen
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
Nach erfolgreicher Migration:
|
||||
|
||||
1. **Auto-Complete implementieren** (optional)
|
||||
- Suche nach häufigen "Wo"-Einträgen
|
||||
- Vorschläge für "Was"-Einträge
|
||||
|
||||
2. **Print-Funktion** (optional)
|
||||
- CSS für Print-Layout
|
||||
- Export als PDF
|
||||
|
||||
3. **Anleitung-Seite**
|
||||
- Kopieren Sie die alte Anleitung
|
||||
- Passen Sie sie an die neue UI an
|
||||
|
||||
4. **Mobile Optimierung**
|
||||
- Responsive Design ist bereits vorhanden
|
||||
- Kann weiter optimiert werden
|
||||
|
||||
5. **Erweiterte Statistiken**
|
||||
- Jahresübersichten
|
||||
- Grafische Darstellungen
|
||||
- Kategorien-Auswertungen
|
||||
|
||||
## Fragen?
|
||||
|
||||
Bei Problemen oder Fragen:
|
||||
- Prüfen Sie [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- Lesen Sie [README.md](README.md)
|
||||
- Kontakt: rxf@gmx.de
|
||||
130
README.md
Normal file
130
README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Ausgaben - Log (Next.js)
|
||||
|
||||
Moderne Ausgaben-Tracking-Anwendung mit Next.js, React, TypeScript und MySQL.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dies ist die modernisierte Version des alten PHP/jQuery-basierten Ausgaben-Programms. Die Anwendung wurde neu in Next.js/React/TypeScript entwickelt und verwendet das gleiche Look & Feel wie die werte-next Anwendung.
|
||||
|
||||
## Features
|
||||
|
||||
- **Eingabe-Tab**: 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)
|
||||
- OK-Checkbox für Kontrolle
|
||||
- Monatsstatistiken
|
||||
- Letzte 10 Einträge
|
||||
|
||||
- **Listen-Tab**: Vollständige Auflistung aller Einträge mit:
|
||||
- Bearbeiten-Funktion
|
||||
- Löschen-Funktion
|
||||
- Sortierung nach Datum (absteigend)
|
||||
|
||||
- **Statistik-Tab**: Monatliche Auswertungen mit:
|
||||
- Gesamtausgaben
|
||||
- Aufschlüsselung nach Zahlungsart
|
||||
- Einnahmen
|
||||
- Überweisungen
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Frontend**: Next.js 16, React 19, TypeScript
|
||||
- **Styling**: Tailwind CSS v4
|
||||
- **Datenbank**: MySQL 8 (via Docker)
|
||||
- **ORM**: mysql2
|
||||
|
||||
## Installation
|
||||
|
||||
1. Repository klonen / Dateien kopieren
|
||||
|
||||
2. Dependencies installieren:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Umgebungsvariablen konfigurieren:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Bearbeiten Sie `.env` und passen Sie die Datenbankverbindung an:
|
||||
```
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=ihr_passwort
|
||||
DB_NAME=RXF
|
||||
```
|
||||
|
||||
4. Datenbank-Tabelle erstellen (falls noch nicht vorhanden):
|
||||
```bash
|
||||
mysql -u root -p RXF < create_table.sql
|
||||
```
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Development-Server starten:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter [http://localhost:3000](http://localhost:3000) erreichbar.
|
||||
|
||||
## Production Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## Datenbank-Schema
|
||||
|
||||
Die Anwendung verwendet die Tabelle `Ausgaben_Tag` mit folgenden Feldern:
|
||||
|
||||
- `ID` (auto_increment)
|
||||
- `Datum` (date)
|
||||
- `WochTag` (varchar)
|
||||
- `Wo` (varchar) - Geschäft/Ort
|
||||
- `Was` (varchar) - Beschreibung
|
||||
- `Wieviel` (decimal) - Betrag
|
||||
- `Wie` (varchar) - Zahlungsart
|
||||
- `OK` (tinyint) - Kontrollstatus
|
||||
|
||||
## 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
|
||||
- `DELETE /api/ausgaben/[id]` - Eintrag löschen
|
||||
- `GET /api/ausgaben/stats` - Monatsstatistiken (mit month, year params)
|
||||
|
||||
## Migration von der alten Version
|
||||
|
||||
Die alte PHP/jQuery-Version in `../Ausgaben` kann parallel weiter betrieben werden. Die Datenbank wird von beiden Anwendungen verwendet.
|
||||
|
||||
### Unterschiede zur alten Version:
|
||||
|
||||
- Moderne React-basierte UI statt jQuery
|
||||
- TypeScript statt JavaScript
|
||||
- REST API statt direkte PHP-Aufrufe
|
||||
- Tailwind CSS statt custom CSS
|
||||
- Client-side Rendering statt Server-side (PHP)
|
||||
|
||||
## Look & Feel
|
||||
|
||||
Das Design orientiert sich an der `werte-next` Anwendung:
|
||||
- Gelber Hintergrund (#FFFFDD)
|
||||
- Schwarze Rahmen um Hauptcontainer
|
||||
- Blaue Akzente für Eingabebereich (#CCCCFF)
|
||||
- Klare Tabellenstruktur
|
||||
- Responsive Design
|
||||
|
||||
## Autor
|
||||
|
||||
rxf@gmx.de
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0
|
||||
82
app/api/ausgaben/[id]/route.ts
Normal file
82
app/api/ausgaben/[id]/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDbPool } from '@/lib/db';
|
||||
import { ResultSetHeader } from 'mysql2';
|
||||
|
||||
// PUT /api/ausgaben/[id] - Update entry
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const body = await request.json();
|
||||
const { Datum, WochTag, Wo, Was, Wieviel, Wie, OK } = body;
|
||||
|
||||
const pool = getDbPool();
|
||||
|
||||
const query = `
|
||||
UPDATE Ausgaben_Tag
|
||||
SET Datum = ?, WochTag = ?, Wo = ?, Was = ?, Wieviel = ?, Wie = ?, OK = ?
|
||||
WHERE ID = ?
|
||||
`;
|
||||
|
||||
const [result] = await pool.query<ResultSetHeader>(query, [
|
||||
Datum,
|
||||
WochTag,
|
||||
Wo,
|
||||
Was,
|
||||
parseFloat(Wieviel),
|
||||
Wie,
|
||||
OK || 0,
|
||||
parseInt(id),
|
||||
]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Entry not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Database error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/ausgaben/[id] - Delete entry
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const pool = getDbPool();
|
||||
|
||||
const query = 'DELETE FROM Ausgaben_Tag WHERE ID = ?';
|
||||
const [result] = await pool.query<ResultSetHeader>(query, [parseInt(id)]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Entry not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Database error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
86
app/api/ausgaben/route.ts
Normal file
86
app/api/ausgaben/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDbPool } from '@/lib/db';
|
||||
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||
|
||||
// GET /api/ausgaben - Fetch entries
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = searchParams.get('limit') || '10';
|
||||
const startDate = searchParams.get('startDate');
|
||||
const month = searchParams.get('month');
|
||||
const year = searchParams.get('year');
|
||||
|
||||
const pool = getDbPool();
|
||||
|
||||
let query = 'SELECT * FROM Ausgaben_Tag';
|
||||
const params: any[] = [];
|
||||
|
||||
if (month && year) {
|
||||
query += ' WHERE YEAR(Datum) = ? AND MONTH(Datum) = ?';
|
||||
params.push(year, month);
|
||||
} else if (startDate) {
|
||||
query += ' WHERE Datum >= ?';
|
||||
params.push(startDate);
|
||||
}
|
||||
|
||||
query += ' ORDER BY Datum DESC, ID DESC LIMIT ?';
|
||||
params.push(parseInt(limit));
|
||||
|
||||
const [rows] = await pool.query<RowDataPacket[]>(query, params);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Database error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/ausgaben - Create new entry
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { Datum, WochTag, Wo, Was, Wieviel, Wie, OK } = body;
|
||||
|
||||
if (!Datum || !Wo || !Was || !Wieviel || !Wie) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const pool = getDbPool();
|
||||
|
||||
const query = `
|
||||
INSERT INTO Ausgaben_Tag (Datum, WochTag, Wo, Was, Wieviel, Wie, OK)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const [result] = await pool.query<ResultSetHeader>(query, [
|
||||
Datum,
|
||||
WochTag,
|
||||
Wo,
|
||||
Was,
|
||||
parseFloat(Wieviel),
|
||||
Wie,
|
||||
OK || 0,
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id: result.insertId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Database error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
69
app/api/ausgaben/stats/route.ts
Normal file
69
app/api/ausgaben/stats/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDbPool } from '@/lib/db';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
// GET /api/ausgaben/stats - Get monthly statistics
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const month = searchParams.get('month');
|
||||
const year = searchParams.get('year');
|
||||
|
||||
if (!month || !year) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Month and year 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_Tag
|
||||
WHERE YEAR(Datum) = ? AND MONTH(Datum) = ?
|
||||
`;
|
||||
|
||||
const [rows] = await pool.query<RowDataPacket[]>(query, [year, month]);
|
||||
|
||||
const data = rows[0] || {
|
||||
totalAusgaben: 0,
|
||||
ECR: 0,
|
||||
ECB: 0,
|
||||
barR: 0,
|
||||
barB: 0,
|
||||
Einnahmen: 0,
|
||||
Ueberweisungen: 0,
|
||||
};
|
||||
|
||||
// Convert string values from MySQL to numbers
|
||||
const parsedData = {
|
||||
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,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: parsedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Database error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/globals.css
Normal file
18
app/globals.css
Normal file
@@ -0,0 +1,18 @@
|
||||
/* stylelint-disable at-rule-no-unknown */
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
21
app/layout.tsx
Normal file
21
app/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Ausgaben - Log",
|
||||
description: "Ausgaben-Tracking Anwendung",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
89
app/page.tsx
Normal file
89
app/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import AusgabenForm from '@/components/AusgabenForm';
|
||||
import AusgabenList from '@/components/AusgabenList';
|
||||
import { AusgabenEntry } from '@/types/ausgaben';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
export default function Home() {
|
||||
const [entries, setEntries] = useState<AusgabenEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedEntry, setSelectedEntry] = useState<AusgabenEntry | null>(null);
|
||||
|
||||
const version = packageJson.version;
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecentEntries();
|
||||
}, []);
|
||||
|
||||
const fetchRecentEntries = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/ausgaben?limit=10', {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setEntries(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching entries:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setSelectedEntry(null);
|
||||
setTimeout(() => {
|
||||
fetchRecentEntries();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setEntries(entries.filter(entry => entry.ID !== id));
|
||||
};
|
||||
|
||||
const handleEdit = (entry: AusgabenEntry) => {
|
||||
setSelectedEntry(entry);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-7xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
||||
<h1 className="text-3xl font-bold text-center mb-6">Ausgaben - Log</h1>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
||||
<AusgabenForm onSuccess={handleSuccess} selectedEntry={selectedEntry} />
|
||||
|
||||
<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>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">Lade Daten...</div>
|
||||
) : (
|
||||
<AusgabenList entries={entries} onDelete={handleDelete} onEdit={handleEdit} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4 border-t-2 border-black pt-4">
|
||||
<div>
|
||||
<a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline">
|
||||
mailto:rxf@gmx.de
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
Version {version}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
335
components/AusgabenForm.tsx
Normal file
335
components/AusgabenForm.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CreateAusgabenEntry, AusgabenEntry, ZAHLUNGSARTEN, Zahlungsart, MonthlyStats } from '@/types/ausgaben';
|
||||
|
||||
interface AusgabenFormProps {
|
||||
onSuccess: () => void;
|
||||
selectedEntry?: AusgabenEntry | null;
|
||||
}
|
||||
|
||||
export default function AusgabenForm({ onSuccess, selectedEntry }: AusgabenFormProps) {
|
||||
const [formData, setFormData] = useState<CreateAusgabenEntry>({
|
||||
Datum: '',
|
||||
WochTag: '',
|
||||
Wo: '',
|
||||
Was: '',
|
||||
Wieviel: '',
|
||||
Wie: 'EC-R',
|
||||
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);
|
||||
|
||||
// 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) => {
|
||||
if (!y || !m) return;
|
||||
|
||||
setIsLoadingStats(true);
|
||||
try {
|
||||
const response = await fetch(`/api/ausgaben/stats?year=${y}&month=${m}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setStats(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
} finally {
|
||||
setIsLoadingStats(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthChange = (newMonth: string) => {
|
||||
setMonth(newMonth);
|
||||
fetchStats(year, newMonth);
|
||||
};
|
||||
|
||||
const handleYearChange = (newYear: string) => {
|
||||
setYear(newYear);
|
||||
fetchStats(newYear, month);
|
||||
};
|
||||
|
||||
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,
|
||||
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,
|
||||
}));
|
||||
|
||||
setEditId(null);
|
||||
}
|
||||
}, [selectedEntry]);
|
||||
|
||||
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: 'EC-R',
|
||||
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 as Zahlungsart })}
|
||||
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-gray-300">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
102
components/AusgabenList.tsx
Normal file
102
components/AusgabenList.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { AusgabenEntry } from '@/types/ausgaben';
|
||||
|
||||
interface AusgabenListProps {
|
||||
entries: AusgabenEntry[];
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (entry: AusgabenEntry) => void;
|
||||
}
|
||||
|
||||
export default function AusgabenList({ entries, onDelete, onEdit }: AusgabenListProps) {
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/ausgaben/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onDelete(id);
|
||||
} else {
|
||||
alert('Fehler beim Löschen!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Löschen!');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-[#CCCCFF]">
|
||||
<th className="border-b-2 border-black p-2 w-32">Datum</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-48">Was</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center p-4 text-gray-500">
|
||||
Keine Einträge vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
entries.map((entry, index) => (
|
||||
<tr key={entry.ID} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-100'}>
|
||||
<td className="border-y border-black p-2 text-center">
|
||||
{formatDate(entry.Datum)}
|
||||
</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.Was}</td>
|
||||
<td className="border-y border-black p-2 text-right">
|
||||
{formatAmount(entry.Wieviel)}
|
||||
</td>
|
||||
<td className="border-y border-black p-2 text-center">{entry.Wie}</td>
|
||||
<td className="border-y border-black p-2 text-center">
|
||||
<button
|
||||
onClick={() => onEdit(entry)}
|
||||
className="text-blue-600 hover:text-blue-800 px-3 py-1 rounded text-sm mr-2"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(entry.ID)}
|
||||
className="text-red-600 hover:text-red-800 px-3 py-1 rounded text-sm"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
create_table.sql
Normal file
16
create_table.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Tabelle für Ausgaben erstellen
|
||||
-- Diese Tabelle sollte bereits in der Docker MySQL-Datenbank existieren
|
||||
-- Falls nicht, hier ist das CREATE Statement:
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `Ausgaben_Tag` (
|
||||
`ID` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`Datum` date NOT NULL,
|
||||
`WochTag` varchar(20) DEFAULT NULL,
|
||||
`Wo` varchar(255) DEFAULT NULL,
|
||||
`Was` varchar(500) DEFAULT NULL,
|
||||
`Wieviel` decimal(10,2) NOT NULL,
|
||||
`Wie` varchar(50) DEFAULT NULL,
|
||||
`OK` tinyint(1) DEFAULT 0,
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `idx_datum` (`Datum`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
17
docker-compose.local.yml
Normal file
17
docker-compose.local.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Docker Compose für lokale Entwicklung auf Esprimo (ohne Traefik)
|
||||
services:
|
||||
ausgaben-app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
BUILD_DATE: ${BUILD_DATE:-$(date +%d.%m.%Y)}
|
||||
container_name: ausgaben-next-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "0.0.0.0:3005:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
- DB_NAME=${DB_NAME}
|
||||
16
eslint.config.mjs
Normal file
16
eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
18
lib/db.ts
Normal file
18
lib/db.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getDbPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'RXF',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6716
package-lock.json
generated
Normal file
6716
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ausgaben_next",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3005",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3005",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "^3.17.4",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
42
setup.sh
Executable file
42
setup.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ausgaben-Next Setup Script
|
||||
|
||||
echo "==================================="
|
||||
echo "Ausgaben-Next Setup"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# Check if .env exists
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "Creating .env file from template..."
|
||||
cp .env.example .env
|
||||
echo "✓ .env created. Please edit it with your database credentials!"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
npm install
|
||||
echo "✓ Dependencies installed"
|
||||
echo ""
|
||||
|
||||
# Build the project
|
||||
echo "Building project..."
|
||||
npm run build
|
||||
echo "✓ Build complete"
|
||||
echo ""
|
||||
|
||||
echo "==================================="
|
||||
echo "Setup complete!"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
echo "To start the development server:"
|
||||
echo " npm run dev"
|
||||
echo ""
|
||||
echo "To start the production server:"
|
||||
echo " npm start"
|
||||
echo ""
|
||||
echo "To build for Docker:"
|
||||
echo " docker-compose -f docker-compose.local.yml up -d"
|
||||
echo ""
|
||||
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
43
types/ausgaben.ts
Normal file
43
types/ausgaben.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// TypeScript interfaces for Ausgaben application
|
||||
|
||||
export interface AusgabenEntry {
|
||||
ID: number;
|
||||
Datum: string;
|
||||
WochTag: string;
|
||||
Wo: string;
|
||||
Was: string;
|
||||
Wieviel: number;
|
||||
Wie: string;
|
||||
OK?: number;
|
||||
}
|
||||
|
||||
export interface CreateAusgabenEntry {
|
||||
Datum: string;
|
||||
WochTag: string;
|
||||
Wo: string;
|
||||
Was: string;
|
||||
Wieviel: string | number;
|
||||
Wie: string;
|
||||
OK?: number;
|
||||
}
|
||||
|
||||
export interface MonthlyStats {
|
||||
totalAusgaben: number;
|
||||
ECR: number;
|
||||
ECB: number;
|
||||
barR: number;
|
||||
barB: number;
|
||||
Einnahmen: number;
|
||||
Ueberweisungen: number;
|
||||
}
|
||||
|
||||
export type Zahlungsart = 'EC-R' | 'EC-B' | 'bar-R' | 'bar-B' | 'Einnahme' | 'Ueber';
|
||||
|
||||
export const ZAHLUNGSARTEN: { value: Zahlungsart; label: string }[] = [
|
||||
{ value: 'EC-R', label: 'EC-R' },
|
||||
{ value: 'EC-B', label: 'EC-B' },
|
||||
{ value: 'bar-R', label: 'bar-R' },
|
||||
{ value: 'bar-B', label: 'bar-B' },
|
||||
{ value: 'Einnahme', label: 'Einnahme' },
|
||||
{ value: 'Ueber', label: 'Überweisung' },
|
||||
];
|
||||
Reference in New Issue
Block a user