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