first commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
WhereIsMyMoney - Personal Finance Management
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
A Go web application for personal finance management using modern frameworks. Features include comprehensive transaction management, multi-edit/delete capabilities, advanced filtering, user authentication, bank account & depot management, and a responsive UI with interactive dashboards.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Backend**: Go 1.23 with Gin web framework
|
||||||
|
- **Templates**: a-h/templ for type-safe HTML templates
|
||||||
|
- **Database**: GORM with SQLite
|
||||||
|
- **Configuration**: Viper for config management
|
||||||
|
- **Sessions**: Gorilla Sessions for user sessions (Safari-compatible)
|
||||||
|
- **Authentication**: bcrypt for password hashing
|
||||||
|
- **Frontend**: Tailwind CSS for styling with modular template structure, Chart.js for data visualization
|
||||||
|
- **JavaScript**: Vanilla JS for dynamic interactions, AJAX operations, and form handling
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- **Transaction Management**: Full CRUD operations with pagination, advanced filtering, multi-edit/delete functionality
|
||||||
|
- **Recurring Transactions**: Automated generation of recurring transactions with flexible intervals
|
||||||
|
- **User Management**: Registration, login, logout with secure session handling and settings management
|
||||||
|
- **Bank Account Management**: Create and manage multiple bank accounts (checking, savings, credit)
|
||||||
|
- **Depot Management**: Create and manage investment depots with different brokers
|
||||||
|
- **Advanced Filtering**: Multi-criteria filtering (date range, description, category, account, type, amount)
|
||||||
|
- **Batch Operations**: Multi-select transactions for bulk editing and deletion
|
||||||
|
- **Interactive Dashboard**: Charts and graphs for financial overview using Chart.js
|
||||||
|
- **Responsive Design**: Mobile-friendly navigation with dropdown menus
|
||||||
|
- **Modular Templates**: Separate components for reusability and maintainability
|
||||||
|
|
||||||
|
## Development
|
||||||
|
- Run server: `go run main.go` or use VS Code task "Run Server"
|
||||||
|
- Generate templates: `templ generate`
|
||||||
|
- Build: `go build -o app .`
|
||||||
|
- Server runs on http://localhost:8080
|
||||||
|
|
||||||
|
## API Routes
|
||||||
|
### Authentication
|
||||||
|
- **Auth**: `/login`, `/register`, `/logout`
|
||||||
|
|
||||||
|
### Main Pages
|
||||||
|
- **Dashboard**: `/` (protected) - Financial overview with interactive charts
|
||||||
|
- **Transactions**: `/transactions` (protected) - Transaction list with filtering, pagination, and multi-operations
|
||||||
|
- **Accounts**: `/accounts` (protected) - Bank account & depot management
|
||||||
|
- **Recurring**: `/recurring` (protected) - Recurring transaction management
|
||||||
|
- **Settings**: `/settings` (protected) - User settings and password management
|
||||||
|
|
||||||
|
### Transaction API
|
||||||
|
- **GET** `/transactions` - Transaction list with pagination and filtering
|
||||||
|
- **POST** `/transactions` - Create new transaction
|
||||||
|
- **PUT** `/transactions/:id` - Update transaction
|
||||||
|
- **DELETE** `/transactions/:id` - Delete transaction
|
||||||
|
- **PUT** `/transactions/multi-update` - Batch update multiple transactions
|
||||||
|
- **DELETE** `/transactions/multi-delete` - Batch delete multiple transactions
|
||||||
|
|
||||||
|
### Recurring Transactions API
|
||||||
|
- **GET** `/recurring` - List recurring transactions
|
||||||
|
- **POST** `/recurring` - Create recurring transaction
|
||||||
|
- **PUT** `/recurring/:id` - Update recurring transaction
|
||||||
|
- **DELETE** `/recurring/:id` - Delete recurring transaction
|
||||||
|
|
||||||
|
### Account Management API
|
||||||
|
- **GET** `/accounts` - View accounts overview
|
||||||
|
- **POST** `/accounts/bank` - Create bank account
|
||||||
|
- **POST** `/accounts/depot` - Create depot
|
||||||
|
|
||||||
|
### User Management API
|
||||||
|
- **PUT** `/settings/profile` - Update user profile (name only)
|
||||||
|
- **PUT** `/settings/password` - Change user password
|
||||||
|
|
||||||
|
## Database Models
|
||||||
|
- **User**: Authentication and user data with relationships to accounts/depots/transactions
|
||||||
|
- **Transaction**: Complete transaction data (amount, description, date, category, account, type)
|
||||||
|
- **RecurringTransaction**: Templates for automated recurring transactions with flexible intervals
|
||||||
|
- **BankAccount**: Bank account details (name, bank, IBAN, balance, account type)
|
||||||
|
- **Depot**: Investment depot details (name, broker, depot number, total value)
|
||||||
|
- **Category**: Transaction categories with icons for visual organization
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
whereismymoney/
|
||||||
|
├── main.go # Application entry point with routes
|
||||||
|
├── config.yaml # Configuration file
|
||||||
|
├── whereismymoney.db # SQLite database
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── style.css # Custom CSS styles
|
||||||
|
│ └── js/
|
||||||
|
│ └── dashboard-charts.js # Chart.js configuration
|
||||||
|
├── internal/
|
||||||
|
│ ├── config/ # Configuration management
|
||||||
|
│ ├── database/ # Database connection and migrations
|
||||||
|
│ ├── handlers/ # HTTP handlers (auth, transactions, accounts, etc.)
|
||||||
|
│ ├── models/ # GORM models (User, Transaction, BankAccount, etc.)
|
||||||
|
│ └── views/ # Templ templates
|
||||||
|
│ ├── layout.templ # Base layout
|
||||||
|
│ ├── navigation.templ # Reusable navigation component
|
||||||
|
│ ├── dashboard.templ # Main dashboard with charts
|
||||||
|
│ ├── transactions.templ # Transaction list with filtering and multi-ops
|
||||||
|
│ ├── accounts.templ # Bank accounts & depots management
|
||||||
|
│ ├── recurring.templ # Recurring transactions management
|
||||||
|
│ ├── settings.templ # User settings and password management
|
||||||
|
│ ├── login.templ # Login page
|
||||||
|
│ └── register.templ # Registration page
|
||||||
|
└── README.md # Documentation
|
||||||
|
```
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Run Server",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go run main.go",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# WhereIsMyMoney
|
||||||
|
|
||||||
|
Eine moderne Webanwendung zur persönlichen Finanzverwaltung mit umfassenden Funktionen für Transaktionsmanagement und Finanzübersicht.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Transaktionsmanagement
|
||||||
|
- **Vollständige CRUD-Operationen**: Erstellen, Bearbeiten, Löschen von Transaktionen
|
||||||
|
- **Multi-Edit-Funktionalität**: Mehrere Transaktionen gleichzeitig bearbeiten
|
||||||
|
- **Multi-Delete-Funktionalität**: Mehrere Transaktionen gleichzeitig löschen
|
||||||
|
- **Erweiterte Filter**: Nach Datum, Beschreibung, Kategorie, Konto, Typ und Betrag filtern
|
||||||
|
- **Paginierung**: Übersichtliche Navigation durch große Transaktionslisten
|
||||||
|
- **Regelmäßige Transaktionen**: Automatische Generierung wiederkehrender Transaktionen
|
||||||
|
|
||||||
|
### Konten & Kategorien
|
||||||
|
- **Bankkonten-Management**: Verwaltung mehrerer Bankkonten mit verschiedenen Typen
|
||||||
|
- **Depot-Management**: Verwaltung von Investment-Depots verschiedener Broker
|
||||||
|
- **Kategorien-System**: Flexible Kategorisierung mit Icons für bessere Übersicht
|
||||||
|
- **Übersicht über Einnahmen und Ausgaben**: Detaillierte Finanzanalyse
|
||||||
|
|
||||||
|
### Benutzerfreundlichkeit
|
||||||
|
- **Responsive Design**: Optimiert für Desktop und Mobile
|
||||||
|
- **Interaktive Dashboards**: Grafische Darstellung der Finanzdaten mit Chart.js
|
||||||
|
- **Benutzereinstellungen**: Passwort-Management und Profilverwaltung
|
||||||
|
- **Session-Management**: Sichere Benutzeranmeldung mit Safari-Kompatibilität
|
||||||
|
|
||||||
|
### Technische Features
|
||||||
|
- **Type-safe Templates**: a-h/templ für sichere HTML-Generierung
|
||||||
|
- **Moderne Architektur**: GORM für Datenbankoperationen
|
||||||
|
- **RESTful API**: JSON-basierte Endpunkte für AJAX-Operationen
|
||||||
|
- **Filter-Persistierung**: URL-Parameter bleiben bei Navigation erhalten
|
||||||
|
|
||||||
|
## Technologien
|
||||||
|
|
||||||
|
- **Backend**: Go 1.23 mit Gin Framework
|
||||||
|
- **Templates**: a-h/templ für type-safe HTML Templates
|
||||||
|
- **Database**: GORM mit SQLite
|
||||||
|
- **Configuration**: Viper für Konfigurationsmanagement
|
||||||
|
- **Sessions**: Gorilla Sessions
|
||||||
|
- **Frontend**: Tailwind CSS
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Repository klonen oder herunterladen
|
||||||
|
2. In das Projektverzeichnis wechseln:
|
||||||
|
```bash
|
||||||
|
cd whereismymoney
|
||||||
|
```
|
||||||
|
3. Dependencies installieren:
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
4. Templates generieren:
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
5. Server starten:
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
## API Routes
|
||||||
|
|
||||||
|
### Authentifizierung
|
||||||
|
- **Auth**: `/login`, `/register`, `/logout`
|
||||||
|
|
||||||
|
### Hauptseiten
|
||||||
|
- **Dashboard**: `/` (geschützt) - Finanzübersicht mit Grafiken
|
||||||
|
- **Transaktionen**: `/transactions` (geschützt) - Transaktionsliste mit Filtern
|
||||||
|
- **Konten**: `/accounts` (geschützt) - Bank- und Depot-Verwaltung
|
||||||
|
- **Einstellungen**: `/settings` (geschützt) - Benutzereinstellungen
|
||||||
|
|
||||||
|
### Transaktions-API
|
||||||
|
- **GET** `/transactions` - Transaktionsliste mit Paginierung und Filtern
|
||||||
|
- **POST** `/transactions` - Neue Transaktion erstellen
|
||||||
|
- **PUT** `/transactions/:id` - Transaktion bearbeiten
|
||||||
|
- **DELETE** `/transactions/:id` - Transaktion löschen
|
||||||
|
- **PUT** `/transactions/multi-update` - Mehrere Transaktionen bearbeiten
|
||||||
|
- **DELETE** `/transactions/multi-delete` - Mehrere Transaktionen löschen
|
||||||
|
|
||||||
|
### Recurring Transactions
|
||||||
|
- **GET** `/recurring` - Regelmäßige Transaktionen anzeigen
|
||||||
|
- **POST** `/recurring` - Neue regelmäßige Transaktion erstellen
|
||||||
|
- **PUT** `/recurring/:id` - Regelmäßige Transaktion bearbeiten
|
||||||
|
- **DELETE** `/recurring/:id` - Regelmäßige Transaktion löschen
|
||||||
|
|
||||||
|
6. Browser öffnen: http://localhost:8080
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
### Templates generieren
|
||||||
|
```bash
|
||||||
|
templ generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server starten
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build erstellen
|
||||||
|
```bash
|
||||||
|
go build -o app .
|
||||||
|
./app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Die App verwendet eine `config.yaml` Datei für die Konfiguration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
host: localhost
|
||||||
|
port: "8080"
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: sqlite
|
||||||
|
path: "./whereismymoney.db"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projekt-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
whereismymoney/
|
||||||
|
├── main.go # Hauptanwendung mit Routing
|
||||||
|
├── config.yaml # Konfigurationsdatei
|
||||||
|
├── whereismymoney.db # SQLite Datenbank
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── style.css # Custom CSS
|
||||||
|
│ └── js/
|
||||||
|
│ └── dashboard-charts.js # Chart.js Konfiguration
|
||||||
|
├── internal/
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── config.go # Konfigurationsverwaltung
|
||||||
|
│ ├── database/
|
||||||
|
│ │ └── database.go # Datenbankverbindung und Migrationen
|
||||||
|
│ ├── handlers/
|
||||||
|
│ │ └── handlers.go # HTTP Handler (Auth, Transaktionen, etc.)
|
||||||
|
│ ├── models/
|
||||||
|
│ │ └── models.go # GORM Modelle (User, Transaction, etc.)
|
||||||
|
│ └── views/ # Templ Templates
|
||||||
|
│ ├── layout.templ # Basis-Layout
|
||||||
|
│ ├── navigation.templ # Navigation-Komponente
|
||||||
|
│ ├── dashboard.templ # Dashboard mit Grafiken
|
||||||
|
│ ├── transactions.templ # Transaktionsliste mit Filtern
|
||||||
|
│ ├── accounts.templ # Konten-Management
|
||||||
|
│ ├── recurring.templ # Regelmäßige Transaktionen
|
||||||
|
│ ├── settings.templ # Benutzereinstellungen
|
||||||
|
│ ├── login.templ # Login-Seite
|
||||||
|
│ └── register.templ # Registrierung
|
||||||
|
└── README.md # Diese Datei
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datenbank-Modelle
|
||||||
|
|
||||||
|
### User
|
||||||
|
- Benutzerauthentifizierung mit bcrypt
|
||||||
|
- Beziehungen zu Konten, Depots und Transaktionen
|
||||||
|
|
||||||
|
### Transaction
|
||||||
|
- Vollständige Transaktionsdaten (Betrag, Beschreibung, Datum, etc.)
|
||||||
|
- Verknüpfung zu Benutzern, Kategorien und Konten
|
||||||
|
- Unterstützung für Ein- und Ausgaben
|
||||||
|
|
||||||
|
### RecurringTransaction
|
||||||
|
- Vorlage für regelmäßige Transaktionen
|
||||||
|
- Flexible Intervalle (täglich, wöchentlich, monatlich, jährlich)
|
||||||
|
- Automatische Generierung neuer Transaktionen
|
||||||
|
|
||||||
|
### BankAccount & Depot
|
||||||
|
- Verschiedene Kontotypen (Girokonto, Sparkonto, Kreditkarte)
|
||||||
|
- Depot-Management für verschiedene Broker
|
||||||
|
- Saldo-Verfolgung
|
||||||
|
|
||||||
|
### Category
|
||||||
|
- Kategorisierung mit Icons
|
||||||
|
- Flexible Zuordnung zu Transaktionen
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# WhereIsMyMoney - Feature Roadmap & TODO Liste
|
||||||
|
|
||||||
|
## 🎯 Priorität: Hoch
|
||||||
|
|
||||||
|
### 📊 Dashboard & Analytics
|
||||||
|
- [ ] **Erweiterte Statistiken**
|
||||||
|
- Monatliche Trends und Vergleiche
|
||||||
|
- Ausgaben nach Kategorien (Pie Chart)
|
||||||
|
- Ein-/Ausgaben Verlauf (Line Chart)
|
||||||
|
- Durchschnittliche monatliche Ausgaben
|
||||||
|
- Sparrate berechnen und anzeigen
|
||||||
|
|
||||||
|
- [ ] **Budget-Management**
|
||||||
|
- Monatliche Budgets pro Kategorie festlegen
|
||||||
|
- Budget-Fortschritt anzeigen (Progress Bars)
|
||||||
|
- Warnungen bei Budget-Überschreitung
|
||||||
|
- Budget vs. tatsächliche Ausgaben Vergleich
|
||||||
|
|
||||||
|
### 📱 Mobile Optimierung
|
||||||
|
- [ ] **Progressive Web App (PWA)**
|
||||||
|
- Service Worker für Offline-Funktionalität
|
||||||
|
- App Manifest für "Add to Home Screen"
|
||||||
|
- Push-Benachrichtigungen für Budget-Warnungen
|
||||||
|
- Touch-optimierte Eingabefelder
|
||||||
|
|
||||||
|
- [ ] **Mobile UI Verbesserungen**
|
||||||
|
- Swipe-Gesten für Transaktionsaktionen
|
||||||
|
- Floating Action Button für neue Transaktionen
|
||||||
|
- Verbessertes Touch-Interface für Filter
|
||||||
|
- Optimierte Tabellen für kleine Bildschirme
|
||||||
|
|
||||||
|
## 🎯 Priorität: Mittel
|
||||||
|
|
||||||
|
### 🔄 Import/Export Funktionen
|
||||||
|
- [ ] **CSV Import**
|
||||||
|
- Bank-CSV Dateien importieren
|
||||||
|
- Mapping-Interface für Spalten
|
||||||
|
- Duplikat-Erkennung und -Behandlung
|
||||||
|
- Vorschau vor dem Import
|
||||||
|
|
||||||
|
- [ ] **Export Funktionen**
|
||||||
|
- Transaktionen als CSV exportieren
|
||||||
|
- PDF-Reports generieren
|
||||||
|
- Jahresabschluss-Reports
|
||||||
|
- Kategorien-Reports
|
||||||
|
|
||||||
|
### 🏦 Erweiterte Konten-Features
|
||||||
|
- [ ] **Kontostand-Tracking**
|
||||||
|
- Automatische Saldo-Berechnung
|
||||||
|
- Kontostand-Historie
|
||||||
|
- Überziehungskredit-Verwaltung
|
||||||
|
- Multi-Währungs-Unterstützung
|
||||||
|
|
||||||
|
- [ ] **Depot-Erweiterungen**
|
||||||
|
- Aktien/ETF Positionen verwalten
|
||||||
|
- Performance-Tracking
|
||||||
|
- Dividenden-Tracking
|
||||||
|
- Portfolio-Diversifikation anzeigen
|
||||||
|
|
||||||
|
### 🔍 Erweiterte Suche & Filter
|
||||||
|
- [ ] **Volltext-Suche**
|
||||||
|
- Suche in Transaktionsbeschreibungen
|
||||||
|
- Fuzzy Search für Tippfehler
|
||||||
|
- Gespeicherte Suchanfragen
|
||||||
|
- Quick-Filter Buttons
|
||||||
|
|
||||||
|
- [ ] **Erweiterte Filter**
|
||||||
|
- Betragsbereiche (von/bis)
|
||||||
|
- Kombinierte Filter (UND/ODER)
|
||||||
|
- Filter-Presets speichern
|
||||||
|
- Erweiterte Datumsfilter (letzte 30 Tage, etc.)
|
||||||
|
|
||||||
|
## 🎯 Priorität: Niedrig
|
||||||
|
|
||||||
|
### 🔔 Benachrichtigungen & Erinnerungen
|
||||||
|
- [ ] **Recurring Transaction Alerts**
|
||||||
|
- E-Mail-Benachrichtigungen für fällige Transaktionen
|
||||||
|
- Push-Notifications im Browser
|
||||||
|
- Erinnerungen für unregelmäßige Ausgaben
|
||||||
|
- Budget-Warnungen
|
||||||
|
|
||||||
|
### 🎨 UI/UX Verbesserungen
|
||||||
|
- [ ] **Themes & Personalisierung**
|
||||||
|
- Dark/Light Mode Toggle
|
||||||
|
- Anpassbare Farben
|
||||||
|
- Benutzer-definierte Kategorien-Icons
|
||||||
|
- Anpassbare Dashboard-Widgets
|
||||||
|
|
||||||
|
- [ ] **Keyboard Shortcuts**
|
||||||
|
- Schnelle Navigation (Strg+1, Strg+2, etc.)
|
||||||
|
- Neue Transaktion anlegen (Strg+N)
|
||||||
|
- Filter öffnen (Strg+F)
|
||||||
|
- Suche fokussieren (/)
|
||||||
|
|
||||||
|
### 🔒 Sicherheit & Backup
|
||||||
|
- [ ] **Backup & Restore**
|
||||||
|
- Automatische Datenbank-Backups
|
||||||
|
- Cloud-Backup Integration
|
||||||
|
- Import/Export der gesamten Datenbank
|
||||||
|
- Backup-Zeitplan konfigurieren
|
||||||
|
|
||||||
|
- [ ] **Erweiterte Sicherheit**
|
||||||
|
- Two-Factor Authentication (2FA)
|
||||||
|
- Session-Timeout konfigurierbar
|
||||||
|
- Login-Versuche limitieren
|
||||||
|
- Passwort-Stärke-Meter
|
||||||
|
|
||||||
|
### 📈 Reporting & Analysis
|
||||||
|
- [ ] **Erweiterte Reports**
|
||||||
|
- Jahresübersicht generieren
|
||||||
|
- Steuer-relevante Ausgaben filtern
|
||||||
|
- Ausgaben-Trends analysieren
|
||||||
|
- Vergleich zwischen Zeiträumen
|
||||||
|
|
||||||
|
- [ ] **Data Visualization**
|
||||||
|
- Interaktive Charts (Zoom, Filter)
|
||||||
|
- Heatmap für Ausgaben-Muster
|
||||||
|
- Sankey-Diagramm für Geldfluss
|
||||||
|
- Forecast-Modelle für zukünftige Ausgaben
|
||||||
|
|
||||||
|
### 🌐 Integration & API
|
||||||
|
- [ ] **Bank-Integration**
|
||||||
|
- Open Banking APIs
|
||||||
|
- Automatischer Transaction Import
|
||||||
|
- Real-time Kontostand-Updates
|
||||||
|
- Bank-spezifische Kategorisierung
|
||||||
|
|
||||||
|
- [ ] **Externe Tools**
|
||||||
|
- YNAB Import/Export
|
||||||
|
- Mint.com Migration
|
||||||
|
- Excel/Google Sheets Sync
|
||||||
|
- Webhook-Support für externe Apps
|
||||||
|
|
||||||
|
## 🔧 Technische Verbesserungen
|
||||||
|
|
||||||
|
### 🏗️ Architektur
|
||||||
|
- [ ] **Performance Optimierung**
|
||||||
|
- Datenbankindexierung verbessern
|
||||||
|
- Lazy Loading für große Datensätze
|
||||||
|
- Caching für häufige Abfragen
|
||||||
|
- API Response Komprimierung
|
||||||
|
|
||||||
|
- [ ] **Testing & Quality**
|
||||||
|
- Unit Tests für alle Handler
|
||||||
|
- Integration Tests für API Endpoints
|
||||||
|
- Frontend Testing mit Playwright
|
||||||
|
- Performance Tests für große Datenmengen
|
||||||
|
|
||||||
|
### 🚀 Deployment & DevOps
|
||||||
|
- [ ] **Docker Containerization**
|
||||||
|
- Multi-stage Dockerfile
|
||||||
|
- Docker Compose für Development
|
||||||
|
- Health Checks implementieren
|
||||||
|
- Environment-basierte Konfiguration
|
||||||
|
|
||||||
|
- [ ] **CI/CD Pipeline**
|
||||||
|
- GitHub Actions Setup
|
||||||
|
- Automatische Tests bei Push
|
||||||
|
- Security Scanning
|
||||||
|
- Automated Deployment
|
||||||
|
|
||||||
|
## 📋 Kleine Verbesserungen
|
||||||
|
|
||||||
|
### 🎯 Quick Wins
|
||||||
|
- [ ] **Benutzerfreundlichkeit**
|
||||||
|
- Lade-Spinner für AJAX-Requests
|
||||||
|
- Toast-Nachrichten für Erfolgsmeldungen
|
||||||
|
- Bessere Fehlermeldungen
|
||||||
|
- Auto-complete für Beschreibungen
|
||||||
|
|
||||||
|
- [ ] **Validation & Error Handling**
|
||||||
|
- Client-side Formvalidierung
|
||||||
|
- Bessere Error Messages
|
||||||
|
- Input-Sanitization verbessern
|
||||||
|
- Graceful Error Recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notizen
|
||||||
|
|
||||||
|
### Aktuell implementierte Features ✅
|
||||||
|
- ✅ Transaktions-CRUD mit Paginierung
|
||||||
|
- ✅ Multi-Edit und Multi-Delete
|
||||||
|
- ✅ Erweiterte Filter-Funktionen
|
||||||
|
- ✅ Recurring Transactions
|
||||||
|
- ✅ Benutzer-Management mit Settings
|
||||||
|
- ✅ Bank- und Depot-Verwaltung
|
||||||
|
- ✅ Responsive Design mit Tailwind CSS
|
||||||
|
- ✅ Dashboard mit Chart.js Integration
|
||||||
|
|
||||||
|
### Nächste Schritte Empfehlung
|
||||||
|
1. **Dashboard Analytics erweitern** - Mehr Charts und Statistiken
|
||||||
|
2. **CSV Import implementieren** - Praktischer Nutzen für Benutzer
|
||||||
|
3. **Mobile Optimierung** - PWA Features hinzufügen
|
||||||
|
4. **Budget-Management** - Core Feature für Finanz-Apps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Letzte Aktualisierung: 26. August 2025*
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
server:
|
||||||
|
host: localhost
|
||||||
|
port: "8080"
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: sqlite
|
||||||
|
path: "./whereismymoney.db"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
module whereismymoney
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.3.943
|
||||||
|
github.com/gin-gonic/gin v1.10.1
|
||||||
|
github.com/gorilla/sessions v1.4.0
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
gorm.io/driver/sqlite v1.6.0
|
||||||
|
gorm.io/gorm v1.30.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/natefinch/atomic v1.0.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.12.0 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
tool github.com/a-h/templ/cmd/templ
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||||
|
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
||||||
|
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||||
|
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||||
|
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||||
|
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `mapstructure:"server"`
|
||||||
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port string `mapstructure:"port"`
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath("./config")
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
viper.SetDefault("server.port", "8080")
|
||||||
|
viper.SetDefault("server.host", "localhost")
|
||||||
|
viper.SetDefault("database.type", "sqlite")
|
||||||
|
viper.SetDefault("database.path", "./data.db")
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
// Config file not found, use defaults
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := viper.Unmarshal(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"whereismymoney/internal/config"
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Connect(cfg *config.Config) (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(cfg.Database.Path), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-migrate the schema
|
||||||
|
if err := models.Migrate(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,336 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User repräsentiert einen Benutzer des Systems
|
||||||
|
type User struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||||
|
PasswordHash string `json:"-" gorm:"not null"` // JSON-Tag "-" versteckt das Feld in API-Responses
|
||||||
|
BankAccounts []BankAccount `gorm:"foreignKey:UserID" json:"bank_accounts,omitempty"`
|
||||||
|
Depots []Depot `gorm:"foreignKey:UserID" json:"depots,omitempty"`
|
||||||
|
Transactions []Transaction `gorm:"foreignKey:UserID" json:"transactions,omitempty"`
|
||||||
|
Categories []Category `gorm:"foreignKey:UserID" json:"categories,omitempty"`
|
||||||
|
RecurrenceRules []RecurrenceRule `gorm:"foreignKey:UserID" json:"recurrence_rules,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BankAccount repräsentiert ein Bankkonto eines Benutzers
|
||||||
|
type BankAccount struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Bank string `json:"bank" gorm:"not null"`
|
||||||
|
IBAN string `json:"iban"`
|
||||||
|
Balance float64 `json:"balance" gorm:"default:0"`
|
||||||
|
AccountType string `json:"account_type" gorm:"not null"` // "checking", "savings", "credit"
|
||||||
|
Transactions []Transaction `gorm:"foreignKey:BankAccountID" json:"transactions,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depot repräsentiert ein Wertpapierdepot eines Benutzers
|
||||||
|
type Depot struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Broker string `json:"broker" gorm:"not null"`
|
||||||
|
DepotNumber string `json:"depot_number"`
|
||||||
|
TotalValue float64 `json:"total_value" gorm:"default:0"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category repräsentiert eine Transaktionskategorie
|
||||||
|
type Category struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Color string `json:"color" gorm:"default:'#6B7280'"` // Default-Farbe
|
||||||
|
Icon string `json:"icon" gorm:"default:'💰'"` // Default-Icon
|
||||||
|
Transactions []Transaction `gorm:"foreignKey:CategoryID" json:"transactions,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction repräsentiert eine Finanztransaktion
|
||||||
|
type Transaction struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
BankAccountID *uint `json:"bank_account_id"` // Optional: kann NULL sein für Depot-Transaktionen
|
||||||
|
BankAccount *BankAccount `gorm:"foreignKey:BankAccountID" json:"bank_account,omitempty"`
|
||||||
|
CategoryID *uint `json:"category_id"` // Optional
|
||||||
|
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
|
Amount float64 `json:"amount" gorm:"not null"`
|
||||||
|
Description string `json:"description" gorm:"not null"`
|
||||||
|
Type string `json:"type" gorm:"not null"` // "income" oder "expense"
|
||||||
|
Date time.Time `json:"date" gorm:"not null"`
|
||||||
|
IsRecurring bool `json:"is_recurring" gorm:"default:false"`
|
||||||
|
RecurrenceRuleID *uint `json:"recurrence_rule_id"` // Optional: Link zur Wiederholungsregel
|
||||||
|
RecurrenceRule *RecurrenceRule `gorm:"foreignKey:RecurrenceRuleID" json:"recurrence_rule,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecurrenceRule definiert die Regeln für wiederkehrende Transaktionen
|
||||||
|
type RecurrenceRule struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
|
||||||
|
// Template-Informationen für neue Transaktionen
|
||||||
|
Amount float64 `json:"amount" gorm:"not null"`
|
||||||
|
Description string `json:"description" gorm:"not null"`
|
||||||
|
CategoryID *uint `json:"category_id"`
|
||||||
|
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
|
Type string `json:"type" gorm:"not null"` // "income" or "expense"
|
||||||
|
BankAccountID *uint `json:"bank_account_id"`
|
||||||
|
BankAccount *BankAccount `gorm:"foreignKey:BankAccountID" json:"bank_account,omitempty"`
|
||||||
|
|
||||||
|
// Wiederholungsregeln
|
||||||
|
Interval string `json:"interval" gorm:"not null"` // "daily", "weekly", "monthly", "yearly"
|
||||||
|
IntervalCount int `json:"interval_count" gorm:"default:1"` // z.B. alle 2 Wochen = weekly + 2
|
||||||
|
StartDate time.Time `json:"start_date" gorm:"not null"`
|
||||||
|
EndDate *time.Time `json:"end_date"` // Optional: NULL bedeutet unbegrenzt
|
||||||
|
DayOfWeek *int `json:"day_of_week"` // 0=Sonntag, 1=Montag, etc. (für wöchentlich)
|
||||||
|
DayOfMonth *int `json:"day_of_month"` // 1-31 (für monatlich)
|
||||||
|
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||||
|
LastGenerated *time.Time `json:"last_generated"` // Letztes generiertes Datum
|
||||||
|
|
||||||
|
// Beziehungen zu generierten Transaktionen
|
||||||
|
Transactions []Transaction `gorm:"foreignKey:RecurrenceRuleID" json:"transactions,omitempty"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard-spezifische Strukturen für Auswertungen
|
||||||
|
|
||||||
|
// AssetOverview stellt eine Vermögensübersicht dar
|
||||||
|
type AssetOverview struct {
|
||||||
|
TotalBankBalance float64 `json:"total_bank_balance"`
|
||||||
|
TotalDepotValue float64 `json:"total_depot_value"`
|
||||||
|
TotalAssets float64 `json:"total_assets"`
|
||||||
|
BankAccounts []BankAccountSummary `json:"bank_accounts"`
|
||||||
|
Depots []DepotSummary `json:"depots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BankAccountSummary für Dashboard-Übersicht
|
||||||
|
type BankAccountSummary struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Bank string `json:"bank"`
|
||||||
|
AccountType string `json:"account_type"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepotSummary für Dashboard-Übersicht
|
||||||
|
type DepotSummary struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Broker string `json:"broker"`
|
||||||
|
TotalValue float64 `json:"total_value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonthlyStats für monatliche Statistiken
|
||||||
|
type MonthlyStats struct {
|
||||||
|
Month string `json:"month"`
|
||||||
|
Income float64 `json:"income"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
NetChange float64 `json:"net_change"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeeklyStats für wöchentliche Statistiken
|
||||||
|
type WeeklyStats struct {
|
||||||
|
Week string `json:"week"`
|
||||||
|
Income float64 `json:"income"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
NetChange float64 `json:"net_change"`
|
||||||
|
StartDate time.Time `json:"start_date"`
|
||||||
|
EndDate time.Time `json:"end_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryStats für Kategorie-Auswertungen
|
||||||
|
type CategoryStats struct {
|
||||||
|
CategoryID uint `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionTrend für Verlaufsdaten
|
||||||
|
type TransactionTrend struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Income float64 `json:"income"`
|
||||||
|
Expense float64 `json:"expense"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardData kombiniert alle Dashboard-Daten
|
||||||
|
type DashboardData struct {
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
AssetOverview AssetOverview `json:"asset_overview"`
|
||||||
|
MonthlyStats []MonthlyStats `json:"monthly_stats"`
|
||||||
|
WeeklyStats []WeeklyStats `json:"weekly_stats"`
|
||||||
|
CategoryStats []CategoryStats `json:"category_stats"`
|
||||||
|
TransactionTrend []TransactionTrend `json:"transaction_trend"`
|
||||||
|
RecentTransactions []Transaction `json:"recent_transactions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword erstellt einen bcrypt Hash des Passworts
|
||||||
|
func (u *User) HashPassword(password string) error {
|
||||||
|
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.PasswordHash = string(hashedBytes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPassword vergleicht das eingegebene Passwort mit dem Hash
|
||||||
|
func (u *User) CheckPassword(password string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRecurringTransaction prüft, ob eine Transaktion wiederkehrend ist
|
||||||
|
func (t *Transaction) IsRecurringTransaction() bool {
|
||||||
|
return t.IsRecurring && t.RecurrenceRule != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextOccurrence berechnet das nächste Auftreten einer wiederkehrenden Transaktion
|
||||||
|
func (r *RecurrenceRule) GetNextOccurrence(from time.Time) *time.Time {
|
||||||
|
if !r.IsActive {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var next time.Time
|
||||||
|
|
||||||
|
switch r.Interval {
|
||||||
|
case "daily":
|
||||||
|
next = from.AddDate(0, 0, r.IntervalCount)
|
||||||
|
case "weekly":
|
||||||
|
next = from.AddDate(0, 0, 7*r.IntervalCount)
|
||||||
|
// Wenn ein bestimmter Wochentag gewünscht ist
|
||||||
|
if r.DayOfWeek != nil {
|
||||||
|
daysUntilTargetDay := (*r.DayOfWeek - int(from.Weekday()) + 7) % 7
|
||||||
|
if daysUntilTargetDay == 0 && from.Equal(r.StartDate) {
|
||||||
|
daysUntilTargetDay = 7 * r.IntervalCount
|
||||||
|
}
|
||||||
|
next = from.AddDate(0, 0, daysUntilTargetDay)
|
||||||
|
}
|
||||||
|
case "monthly":
|
||||||
|
next = from.AddDate(0, r.IntervalCount, 0)
|
||||||
|
// Wenn ein bestimmter Tag im Monat gewünscht ist
|
||||||
|
if r.DayOfMonth != nil && *r.DayOfMonth >= 1 && *r.DayOfMonth <= 31 {
|
||||||
|
next = time.Date(next.Year(), next.Month(), *r.DayOfMonth, next.Hour(), next.Minute(), next.Second(), next.Nanosecond(), next.Location())
|
||||||
|
}
|
||||||
|
case "yearly":
|
||||||
|
next = from.AddDate(r.IntervalCount, 0, 0)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob das nächste Datum nach dem Enddatum liegt
|
||||||
|
if r.EndDate != nil && next.After(*r.EndDate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &next
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanGenerate prüft, ob eine neue Instanz der wiederkehrenden Transaktion generiert werden kann
|
||||||
|
func (r *RecurrenceRule) CanGenerate() bool {
|
||||||
|
if !r.IsActive {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Prüfen, ob wir noch vor dem Enddatum sind
|
||||||
|
if r.EndDate != nil && now.After(*r.EndDate) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob es Zeit für die nächste Generierung ist
|
||||||
|
lastDate := r.StartDate
|
||||||
|
if r.LastGenerated != nil {
|
||||||
|
lastDate = *r.LastGenerated
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOccurrence := r.GetNextOccurrence(lastDate)
|
||||||
|
if nextOccurrence == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geändert: Generiere Transaktionen für die nächsten 12 Monate (für Dashboard-Trend)
|
||||||
|
future12Months := now.AddDate(1, 0, 0)
|
||||||
|
if nextOccurrence.After(future12Months) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateTransaction erstellt eine neue Transaktion basierend auf der Wiederholungsregel
|
||||||
|
func (r *RecurrenceRule) GenerateTransaction(db *gorm.DB) (*Transaction, error) {
|
||||||
|
if !r.CanGenerate() {
|
||||||
|
return nil, nil // Keine Transaktion zu generieren
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nächstes Datum berechnen
|
||||||
|
lastDate := r.StartDate
|
||||||
|
if r.LastGenerated != nil {
|
||||||
|
lastDate = *r.LastGenerated
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDate := r.GetNextOccurrence(lastDate)
|
||||||
|
if nextDate == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Transaktion erstellen
|
||||||
|
transaction := &Transaction{
|
||||||
|
UserID: r.UserID,
|
||||||
|
BankAccountID: r.BankAccountID,
|
||||||
|
Amount: r.Amount,
|
||||||
|
Description: r.Description,
|
||||||
|
CategoryID: r.CategoryID,
|
||||||
|
Type: r.Type,
|
||||||
|
Date: *nextDate,
|
||||||
|
IsRecurring: true,
|
||||||
|
RecurrenceRuleID: &r.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaktion in der Datenbank speichern
|
||||||
|
if err := db.Create(transaction).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastGenerated aktualisieren
|
||||||
|
r.LastGenerated = nextDate
|
||||||
|
if err := db.Save(r).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate führt die Datenbank-Migration durch
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(&User{}, &Category{}, &Transaction{}, &BankAccount{}, &Depot{}, &RecurrenceRule{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Accounts(userName string, bankAccounts []models.BankAccount, depots []models.Depot) {
|
||||||
|
@Layout("Konten & Depots - WhereIsMyMoney") {
|
||||||
|
@Navigation(userName)
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Konten & Depots</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Verwalte deine Bankkonten und Wertpapierdepots</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mb-6 flex flex-wrap gap-4">
|
||||||
|
<button
|
||||||
|
onclick="showModal('bankAccountModal')"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||||
|
</svg>
|
||||||
|
Bankkonto hinzufügen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="showModal('depotModal')"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||||
|
</svg>
|
||||||
|
Depot hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- Bankkonten Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Bankkonten</h2>
|
||||||
|
if len(bankAccounts) == 0 {
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="mt-4 text-lg font-medium text-gray-900">Keine Bankkonten</h3>
|
||||||
|
<p class="mt-2 text-gray-500">Füge dein erstes Bankkonto hinzu, um zu beginnen.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="space-y-4">
|
||||||
|
for _, account := range bankAccounts {
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">{ account.Name }</h3>
|
||||||
|
<p class="text-sm text-gray-500">{ account.Bank }</p>
|
||||||
|
<p class="text-sm text-gray-500 capitalize">{ account.AccountType }</p>
|
||||||
|
if account.IBAN != "" {
|
||||||
|
<p class="text-sm text-gray-400 mt-1">{ account.IBAN }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-2xl font-bold text-gray-900">
|
||||||
|
{ fmt.Sprintf("%.2f", account.Balance) } €
|
||||||
|
</p>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Aktiv
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 text-sm font-medium">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Depots Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Depots</h2>
|
||||||
|
if len(depots) == 0 {
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="mt-4 text-lg font-medium text-gray-900">Keine Depots</h3>
|
||||||
|
<p class="mt-2 text-gray-500">Füge dein erstes Depot hinzu, um zu beginnen.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="space-y-4">
|
||||||
|
for _, depot := range depots {
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">{ depot.Name }</h3>
|
||||||
|
<p class="text-sm text-gray-500">{ depot.Broker }</p>
|
||||||
|
if depot.DepotNumber != "" {
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Depot: { depot.DepotNumber }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-2xl font-bold text-gray-900">
|
||||||
|
{ fmt.Sprintf("%.2f", depot.TotalValue) } €
|
||||||
|
</p>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Aktiv
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 text-sm font-medium">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bank Account Modal -->
|
||||||
|
<div id="bankAccountModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Bankkonto hinzufügen</h3>
|
||||||
|
</div>
|
||||||
|
<form action="/accounts/bank" method="POST" class="px-6 py-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="bank-name" class="block text-sm font-medium text-gray-700">Kontoname</label>
|
||||||
|
<input type="text" id="bank-name" name="name" required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="z.B. Hauptkonto">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="bank-bank-name" class="block text-sm font-medium text-gray-700">Bankname</label>
|
||||||
|
<input type="text" id="bank-bank-name" name="bank_name" required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="z.B. Sparkasse">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="bank-account-type" class="block text-sm font-medium text-gray-700">Kontotyp</label>
|
||||||
|
<select id="bank-account-type" name="account_type" required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="">Wähle einen Typ</option>
|
||||||
|
<option value="checking">Girokonto</option>
|
||||||
|
<option value="savings">Sparkonto</option>
|
||||||
|
<option value="credit">Kreditkonto</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="bank-iban" class="block text-sm font-medium text-gray-700">IBAN (optional)</label>
|
||||||
|
<input type="text" id="bank-iban" name="iban"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="DE89 3704 0044 0532 0130 00">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="bank-balance" class="block text-sm font-medium text-gray-700">Aktueller Kontostand</label>
|
||||||
|
<input type="number" id="bank-balance" name="balance" step="0.01"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideModal('bankAccountModal')"
|
||||||
|
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-md">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Depot Modal -->
|
||||||
|
<div id="depotModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Depot hinzufügen</h3>
|
||||||
|
</div>
|
||||||
|
<form action="/accounts/depot" method="POST" class="px-6 py-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="depot-name" class="block text-sm font-medium text-gray-700">Depotname</label>
|
||||||
|
<input type="text" id="depot-name" name="name" required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="z.B. Hauptdepot">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="depot-broker-name" class="block text-sm font-medium text-gray-700">Broker</label>
|
||||||
|
<input type="text" id="depot-broker-name" name="broker_name" required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="z.B. Trade Republic">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="depot-number" class="block text-sm font-medium text-gray-700">Depotnummer (optional)</label>
|
||||||
|
<input type="text" id="depot-number" name="depot_number"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="123456789">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="depot-value" class="block text-sm font-medium text-gray-700">Aktueller Gesamtwert</label>
|
||||||
|
<input type="number" id="depot-value" name="total_value" step="0.01"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideModal('depotModal')"
|
||||||
|
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-md">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md">
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
if (event.target.classList.contains('bg-opacity-50')) {
|
||||||
|
event.target.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,138 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Dashboard(data *models.DashboardData) {
|
||||||
|
@Layout("WhereIsMyMoney") {
|
||||||
|
@Navigation(data.UserName)
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Willkommen zurück, { data.UserName }!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vermögensübersicht -->
|
||||||
|
<div class="mb-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Depots</h3>
|
||||||
|
<p class="text-3xl font-bold text-blue-600">€{ fmt.Sprintf("%.2f", data.AssetOverview.TotalDepotValue) }</p>
|
||||||
|
<p class="text-sm text-gray-500">{ fmt.Sprintf("%d Depots", len(data.AssetOverview.Depots)) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Gesamtvermögen</h3>
|
||||||
|
<p class="text-3xl font-bold text-purple-600">€{ fmt.Sprintf("%.2f", data.AssetOverview.TotalAssets) }</p>
|
||||||
|
<p class="text-sm text-gray-500">Bank + Depot</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts und Statistiken -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
|
<!-- Monatliche Entwicklung -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Monatliche Entwicklung</h3>
|
||||||
|
<div class="h-64" id="monthly-chart">
|
||||||
|
<canvas id="monthlyChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Einnahmen vs Ausgaben Vergleich -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Einnahmen vs Ausgaben</h3>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="incomeExpenseChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verlaufs-Chart -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">12-Monats Vorausschau</h3>
|
||||||
|
<div class="h-80">
|
||||||
|
<canvas id="trendChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detaillierte Übersichten -->
|
||||||
|
<div class="grid grid-cols-1 gap-8 mb-8">
|
||||||
|
<!-- Depot-Details -->
|
||||||
|
if len(data.AssetOverview.Depots) > 0 {
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Depots</h3>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
for _, depot := range data.AssetOverview.Depots {
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-900">{ depot.Name }</p>
|
||||||
|
<p class="text-sm text-gray-500">{ depot.Broker }</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-semibold text-gray-900">€{ fmt.Sprintf("%.2f", depot.TotalValue) }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktuelle Transaktionen -->
|
||||||
|
if len(data.RecentTransactions) > 0 {
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Aktuelle Transaktionen</h3>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
for _, transaction := range data.RecentTransactions {
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-900">{ transaction.Description }</p>
|
||||||
|
<div class="flex items-center text-sm text-gray-500 space-x-2">
|
||||||
|
<span>{ transaction.Date.Format("02.01.2006") }</span>
|
||||||
|
if transaction.Category != nil {
|
||||||
|
<span>•</span>
|
||||||
|
<span>{ transaction.Category.Name }</span>
|
||||||
|
}
|
||||||
|
if transaction.BankAccount != nil {
|
||||||
|
<span>•</span>
|
||||||
|
<span>{ transaction.BankAccount.Name }</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
if transaction.Type == "income" {
|
||||||
|
<p class="text-lg font-semibold text-green-600">+€{ fmt.Sprintf("%.2f", transaction.Amount) }</p>
|
||||||
|
} else {
|
||||||
|
<p class="text-lg font-semibold text-red-600">-€{ fmt.Sprintf("%.2f", transaction.Amount) }</p>
|
||||||
|
}
|
||||||
|
<p class="text-xs text-gray-400">{ transaction.Type }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-3 bg-gray-50 text-center">
|
||||||
|
<a href="/transactions" class="text-sm text-blue-600 hover:text-blue-500">Alle Transaktionen anzeigen →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- JavaScript für Charts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="/static/js/dashboard-charts.js"></script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.778
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dashboard(data *models.DashboardData) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = Navigation(data.UserName).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <!-- Main Content --> <div class=\"min-h-screen bg-gray-50\"><div class=\"max-w-7xl mx-auto py-6 sm:px-6 lg:px-8\"><div class=\"px-4 py-6 sm:px-0\"><!-- Welcome Header --><div class=\"mb-8\"><h1 class=\"text-3xl font-bold text-gray-900\">Dashboard</h1><p class=\"mt-2 text-gray-600\">Willkommen zurück, ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.UserName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 19, Col: 71}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("!</p></div><!-- Vermögensübersicht --><div class=\"mb-8 grid grid-cols-1 md:grid-cols-2 gap-6\"><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-2\">Depots</h3><p class=\"text-3xl font-bold text-blue-600\">€")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", data.AssetOverview.TotalDepotValue))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 26, Col: 111}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Depots", len(data.AssetOverview.Depots)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 27, Col: 98}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-2\">Gesamtvermögen</h3><p class=\"text-3xl font-bold text-purple-600\">€")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", data.AssetOverview.TotalAssets))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 32, Col: 109}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"text-sm text-gray-500\">Bank + Depot</p></div></div><!-- Charts und Statistiken --><div class=\"grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8\"><!-- Monatliche Entwicklung --><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">Monatliche Entwicklung</h3><div class=\"h-64\" id=\"monthly-chart\"><canvas id=\"monthlyChart\"></canvas></div></div><!-- Einnahmen vs Ausgaben Vergleich --><div class=\"bg-white rounded-lg shadow p-6\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">Einnahmen vs Ausgaben</h3><div class=\"h-64\"><canvas id=\"incomeExpenseChart\"></canvas></div></div></div><!-- Verlaufs-Chart --><div class=\"bg-white rounded-lg shadow p-6 mb-8\"><h3 class=\"text-lg font-medium text-gray-900 mb-4\">12-Monats Vorausschau</h3><div class=\"h-80\"><canvas id=\"trendChart\"></canvas></div></div><!-- Detaillierte Übersichten --><div class=\"grid grid-cols-1 gap-8 mb-8\"><!-- Depot-Details -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(data.AssetOverview.Depots) > 0 {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-white rounded-lg shadow\"><div class=\"px-6 py-4 border-b border-gray-200\"><h3 class=\"text-lg font-medium text-gray-900\">Depots</h3></div><div class=\"divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, depot := range data.AssetOverview.Depots {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 py-4\"><div class=\"flex justify-between items-center\"><div><p class=\"text-sm font-medium text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(depot.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 77, Col: 70}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(depot.Broker)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 78, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><p class=\"text-lg font-semibold text-gray-900\">€")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", depot.TotalValue))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 80, Col: 101}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><!-- Aktuelle Transaktionen -->")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(data.RecentTransactions) > 0 {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-white rounded-lg shadow\"><div class=\"px-6 py-4 border-b border-gray-200\"><h3 class=\"text-lg font-medium text-gray-900\">Aktuelle Transaktionen</h3></div><div class=\"divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, transaction := range data.RecentTransactions {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 py-4\"><div class=\"flex justify-between items-center\"><div><p class=\"text-sm font-medium text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 100, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"flex items-center text-sm text-gray-500 space-x-2\"><span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Date.Format("02.01.2006"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 102, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if transaction.Category != nil {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>•</span> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Category.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 105, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transaction.BankAccount != nil {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>•</span> <span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.BankAccount.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 109, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if transaction.Type == "income" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-lg font-semibold text-green-600\">+€")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", transaction.Amount))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 115, Col: 106}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-lg font-semibold text-red-600\">-€")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", transaction.Amount))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 117, Col: 104}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-xs text-gray-400\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(transaction.Type)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/dashboard.templ`, Line: 119, Col: 63}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"px-6 py-3 bg-gray-50 text-center\"><a href=\"/transactions\" class=\"text-sm text-blue-600 hover:text-blue-500\">Alle Transaktionen anzeigen →</a></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!-- JavaScript für Charts --><script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script><script src=\"/static/js/dashboard-charts.js\"></script></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("WhereIsMyMoney").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
templ Layout(title string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50">
|
||||||
|
{ children... }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.778
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Layout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"de\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/layout.templ`, Line: 9, Col: 16}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><script src=\"https://cdn.tailwindcss.com\"></script></head><body class=\"bg-gray-50\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
templ LoginPage(errorMsg string) {
|
||||||
|
@Layout("Anmelden") {
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
Bei WhereIsMyMoney anmelden
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
Oder
|
||||||
|
<a href="/register" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||||
|
neues Konto erstellen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if errorMsg != "" {
|
||||||
|
<div class="rounded-md bg-red-50 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
{ errorMsg }
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form class="mt-8 space-y-6" action="/login" method="POST">
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="sr-only">E-Mail-Adresse</label>
|
||||||
|
<input id="email" name="email" type="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="E-Mail-Adresse"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only">Passwort</label>
|
||||||
|
<input id="password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="remember_me" name="remember_me" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"/>
|
||||||
|
<label for="remember_me" class="ml-2 block text-sm text-gray-900">
|
||||||
|
Angemeldet bleiben
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.778
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func LoginPage(errorMsg string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8\"><div class=\"max-w-md w-full space-y-8\"><div><h2 class=\"mt-6 text-center text-3xl font-extrabold text-gray-900\">Bei WhereIsMyMoney anmelden</h2><p class=\"mt-2 text-center text-sm text-gray-600\">Oder <a href=\"/register\" class=\"font-medium text-indigo-600 hover:text-indigo-500\">neues Konto erstellen</a></p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"rounded-md bg-red-50 p-4\"><div class=\"flex\"><div class=\"flex-shrink-0\"><svg class=\"h-5 w-5 text-red-400\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"></path></svg></div><div class=\"ml-3\"><h3 class=\"text-sm font-medium text-red-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/login.templ`, Line: 29, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h3></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form class=\"mt-8 space-y-6\" action=\"/login\" method=\"POST\"><div class=\"rounded-md shadow-sm -space-y-px\"><div><label for=\"email\" class=\"sr-only\">E-Mail-Adresse</label> <input id=\"email\" name=\"email\" type=\"email\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"E-Mail-Adresse\"></div><div><label for=\"password\" class=\"sr-only\">Passwort</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort\"></div></div><div class=\"flex items-center justify-between\"><div class=\"flex items-center\"><input id=\"remember_me\" name=\"remember_me\" type=\"checkbox\" class=\"h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded\"> <label for=\"remember_me\" class=\"ml-2 block text-sm text-gray-900\">Angemeldet bleiben</label></div></div><div><button type=\"submit\" class=\"group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\">Anmelden</button></div></form></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Anmelden").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
templ Navigation(userName string) {
|
||||||
|
<!-- Header Navigation -->
|
||||||
|
<nav class="bg-white shadow-lg">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<h1 class="text-xl font-bold text-indigo-600">WhereIsMyMoney</h1>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="ml-10 flex items-baseline space-x-4">
|
||||||
|
<a href="/" class="text-gray-900 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Dashboard</a>
|
||||||
|
<a href="/accounts" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Konten & Depots</a>
|
||||||
|
<a href="/transactions" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Transaktionen</a>
|
||||||
|
<a href="/recurring" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Wiederkehrend</a>
|
||||||
|
<a href="/categories" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Kategorien</a>
|
||||||
|
<a href="/reports" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Berichte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="ml-4 flex items-center md:ml-6">
|
||||||
|
<!-- User dropdown -->
|
||||||
|
<div class="relative">
|
||||||
|
<button type="button" class="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 p-1" onclick="toggleUserMenu()" aria-expanded="false" aria-haspopup="true">
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
<div class="h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||||
|
<span class="text-sm font-medium text-white">{ userName[0:1] }</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<div id="user-menu" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" style="display: none;">
|
||||||
|
<div class="px-4 py-2 text-sm text-gray-700 border-b border-gray-100">
|
||||||
|
<div class="font-medium">{ userName }</div>
|
||||||
|
<div class="text-gray-500 text-xs">Angemeldet</div>
|
||||||
|
</div>
|
||||||
|
<a href="/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
Profil
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="mr-3 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Einstellungen
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="border-t border-gray-100">
|
||||||
|
<form action="/logout" method="POST" class="block">
|
||||||
|
<button type="submit" class="w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-red-50 flex items-center" role="menuitem">
|
||||||
|
<svg class="mr-3 h-4 w-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Mobile menu button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button class="bg-gray-50 p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" onclick="toggleMobileMenu()">
|
||||||
|
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile menu -->
|
||||||
|
<div class="md:hidden" id="mobile-menu" style="display: none;">
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||||
|
<a href="/" class="text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium">Dashboard</a>
|
||||||
|
<a href="/accounts" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Konten & Depots</a>
|
||||||
|
<a href="/transactions" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Transaktionen</a>
|
||||||
|
<a href="/categories" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Kategorien</a>
|
||||||
|
<a href="/reports" class="text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium">Berichte</a>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4 pb-3 border-t border-gray-200">
|
||||||
|
<div class="flex items-center px-5">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-gray-800 text-sm font-medium">{ userName }</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<form action="/logout" method="POST" class="inline">
|
||||||
|
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleMobileMenu() {
|
||||||
|
const menu = document.getElementById('mobile-menu');
|
||||||
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserMenu() {
|
||||||
|
const menu = document.getElementById('user-menu');
|
||||||
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const userButton = event.target.closest('[onclick="toggleUserMenu()"]');
|
||||||
|
const userMenu = document.getElementById('user-menu');
|
||||||
|
|
||||||
|
if (!userButton && userMenu && userMenu.style.display === 'block') {
|
||||||
|
userMenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.778
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Navigation(userName string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!-- Header Navigation --><nav class=\"bg-white shadow-lg\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><div class=\"flex justify-between h-16\"><div class=\"flex items-center\"><div class=\"flex-shrink-0\"><h1 class=\"text-xl font-bold text-indigo-600\">WhereIsMyMoney</h1></div><div class=\"hidden md:block\"><div class=\"ml-10 flex items-baseline space-x-4\"><a href=\"/\" class=\"text-gray-900 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Dashboard</a> <a href=\"/accounts\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Konten & Depots</a> <a href=\"/transactions\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Transaktionen</a> <a href=\"/recurring\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Wiederkehrend</a> <a href=\"/categories\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Kategorien</a> <a href=\"/reports\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium\">Berichte</a></div></div></div><div class=\"flex items-center\"><div class=\"ml-4 flex items-center md:ml-6\"><!-- User dropdown --><div class=\"relative\"><button type=\"button\" class=\"bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 p-1\" onclick=\"toggleUserMenu()\" aria-expanded=\"false\" aria-haspopup=\"true\"><span class=\"sr-only\">Open user menu</span><div class=\"h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center\"><span class=\"text-sm font-medium text-white\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(userName[0:1])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 30, Col: 69}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div></button><!-- Dropdown menu --><div id=\"user-menu\" class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50\" role=\"menu\" aria-orientation=\"vertical\" aria-labelledby=\"user-menu-button\" style=\"display: none;\"><div class=\"px-4 py-2 text-sm text-gray-700 border-b border-gray-100\"><div class=\"font-medium\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(userName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 37, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"text-gray-500 text-xs\">Angemeldet</div></div><a href=\"/profile\" class=\"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100\" role=\"menuitem\"><div class=\"flex items-center\"><svg class=\"mr-3 h-4 w-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z\"></path></svg> Profil</div></a> <a href=\"/settings\" class=\"block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100\" role=\"menuitem\"><div class=\"flex items-center\"><svg class=\"mr-3 h-4 w-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z\"></path> <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\"></path></svg> Einstellungen</div></a><div class=\"border-t border-gray-100\"><form action=\"/logout\" method=\"POST\" class=\"block\"><button type=\"submit\" class=\"w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-red-50 flex items-center\" role=\"menuitem\"><svg class=\"mr-3 h-4 w-4 text-red-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1\"></path></svg> Abmelden</button></form></div></div></div></div></div><!-- Mobile menu button --><div class=\"md:hidden flex items-center\"><button class=\"bg-gray-50 p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\" onclick=\"toggleMobileMenu()\"><svg class=\"h-6 w-6\" stroke=\"currentColor\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button></div></div></div><!-- Mobile menu --><div class=\"md:hidden\" id=\"mobile-menu\" style=\"display: none;\"><div class=\"px-2 pt-2 pb-3 space-y-1 sm:px-3\"><a href=\"/\" class=\"text-gray-900 hover:bg-gray-50 block px-3 py-2 rounded-md text-base font-medium\">Dashboard</a> <a href=\"/accounts\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Konten & Depots</a> <a href=\"/transactions\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Transaktionen</a> <a href=\"/categories\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Kategorien</a> <a href=\"/reports\" class=\"text-gray-500 hover:bg-gray-50 hover:text-gray-900 block px-3 py-2 rounded-md text-base font-medium\">Berichte</a></div><div class=\"pt-4 pb-3 border-t border-gray-200\"><div class=\"flex items-center px-5\"><div class=\"flex-shrink-0\"><span class=\"text-gray-800 text-sm font-medium\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(userName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/navigation.templ`, Line: 94, Col: 64}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div><div class=\"ml-auto\"><form action=\"/logout\" method=\"POST\" class=\"inline\"><button type=\"submit\" class=\"bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium\">Abmelden</button></form></div></div></div></div></nav><script>\n\t\tfunction toggleMobileMenu() {\n\t\t\tconst menu = document.getElementById('mobile-menu');\n\t\t\tmenu.style.display = menu.style.display === 'none' ? 'block' : 'none';\n\t\t}\n\n\t\tfunction toggleUserMenu() {\n\t\t\tconst menu = document.getElementById('user-menu');\n\t\t\tmenu.style.display = menu.style.display === 'none' ? 'block' : 'none';\n\t\t}\n\n\t\t// Close dropdown when clicking outside\n\t\tdocument.addEventListener('click', function(event) {\n\t\t\tconst userButton = event.target.closest('[onclick=\"toggleUserMenu()\"]');\n\t\t\tconst userMenu = document.getElementById('user-menu');\n\t\t\t\n\t\t\tif (!userButton && userMenu && userMenu.style.display === 'block') {\n\t\t\t\tuserMenu.style.display = 'none';\n\t\t\t}\n\t\t});\n\t</script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ RecurringTransactions(userName string, rules []models.RecurrenceRule) {
|
||||||
|
@Layout("Wiederkehrende Transaktionen - WhereIsMyMoney") {
|
||||||
|
@Navigation(userName)
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Wiederkehrende Transaktionen</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Verwalten Sie Ihre regelmäßigen Ein- und Ausgaben</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regeln Liste -->
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Ihre wiederkehrenden Transaktionen</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if len(rules) == 0 {
|
||||||
|
<div class="px-6 py-8 text-center">
|
||||||
|
<p class="text-gray-500">Noch keine wiederkehrenden Transaktionen vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
for _, rule := range rules {
|
||||||
|
<div class="px-6 py-4" data-rule-id={ fmt.Sprintf("%d", rule.ID) }>
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<h4 class="text-lg font-medium text-gray-900">{ rule.Description }</h4>
|
||||||
|
if rule.IsActive {
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Aktiv
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
Inaktiv
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex items-center space-x-6 text-sm text-gray-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium">Betrag:</span>
|
||||||
|
if rule.Type == "income" {
|
||||||
|
<span class="ml-1 text-green-600 font-semibold">+€{ fmt.Sprintf("%.2f", rule.Amount) }</span>
|
||||||
|
} else {
|
||||||
|
<span class="ml-1 text-red-600 font-semibold">-€{ fmt.Sprintf("%.2f", rule.Amount) }</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium">Intervall:</span>
|
||||||
|
<span class="ml-1">
|
||||||
|
switch rule.Interval {
|
||||||
|
case "daily":
|
||||||
|
if rule.IntervalCount == 1 {
|
||||||
|
Täglich
|
||||||
|
} else {
|
||||||
|
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Tage
|
||||||
|
}
|
||||||
|
case "weekly":
|
||||||
|
if rule.IntervalCount == 1 {
|
||||||
|
Wöchentlich
|
||||||
|
} else {
|
||||||
|
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Wochen
|
||||||
|
}
|
||||||
|
case "monthly":
|
||||||
|
if rule.IntervalCount == 1 {
|
||||||
|
Monatlich
|
||||||
|
} else {
|
||||||
|
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Monate
|
||||||
|
}
|
||||||
|
case "yearly":
|
||||||
|
if rule.IntervalCount == 1 {
|
||||||
|
Jährlich
|
||||||
|
} else {
|
||||||
|
Alle { fmt.Sprintf("%d", rule.IntervalCount) } Jahre
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{ rule.Interval }
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if rule.Category != nil {
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium">Kategorie:</span>
|
||||||
|
<span class="ml-1">{ rule.Category.Name }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium">Start:</span>
|
||||||
|
<span class="ml-1">{ rule.StartDate.Format("02.01.2006") }</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if rule.EndDate != nil {
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium">Ende:</span>
|
||||||
|
<span class="ml-1">{ rule.EndDate.Format("02.01.2006") }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
|
<button
|
||||||
|
class="edit-rule-btn inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
|
||||||
|
data-description={ rule.Description }
|
||||||
|
data-amount={ fmt.Sprintf("%.2f", rule.Amount) }
|
||||||
|
data-type={ rule.Type }
|
||||||
|
data-interval={ rule.Interval }
|
||||||
|
data-start-date={ rule.StartDate.Format("2006-01-02") }
|
||||||
|
if rule.EndDate != nil {
|
||||||
|
data-end-date={ rule.EndDate.Format("2006-01-02") }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="toggle-rule-btn inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
|
||||||
|
data-active={ fmt.Sprintf("%t", rule.IsActive) }
|
||||||
|
>
|
||||||
|
if rule.IsActive {
|
||||||
|
Deaktivieren
|
||||||
|
} else {
|
||||||
|
Aktivieren
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="delete-rule-btn inline-flex items-center px-3 py-1 border border-red-300 shadow-sm text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||||
|
data-rule-id={ fmt.Sprintf("%d", rule.ID) }
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal (wird per JavaScript eingeblendet) -->
|
||||||
|
<div id="editModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||||
|
<div class="mt-3">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Wiederkehrende Transaktion bearbeiten</h3>
|
||||||
|
|
||||||
|
<form id="editForm">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Beschreibung</label>
|
||||||
|
<input type="text" id="editDescription" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Betrag (€)</label>
|
||||||
|
<input type="number" step="0.01" id="editAmount" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Typ</label>
|
||||||
|
<select id="editType" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="income">Einnahme</option>
|
||||||
|
<option value="expense">Ausgabe</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Intervall</label>
|
||||||
|
<select id="editInterval" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="daily">Täglich</option>
|
||||||
|
<option value="weekly">Wöchentlich</option>
|
||||||
|
<option value="monthly">Monatlich</option>
|
||||||
|
<option value="yearly">Jährlich</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Startdatum</label>
|
||||||
|
<input type="date" id="editStartDate" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Enddatum (optional)</label>
|
||||||
|
<input type="date" id="editEndDate" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Leer lassen für unbegrenzte Laufzeit</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
|
<button type="button" id="cancelEdit" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-300 rounded-md hover:bg-gray-400">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript für Interaktionen -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Edit Modal Funktionalität
|
||||||
|
const modal = document.getElementById('editModal');
|
||||||
|
const editForm = document.getElementById('editForm');
|
||||||
|
const cancelBtn = document.getElementById('cancelEdit');
|
||||||
|
let currentRuleId = null;
|
||||||
|
|
||||||
|
// Edit Button Event Listener
|
||||||
|
document.querySelectorAll('.edit-rule-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
currentRuleId = this.dataset.ruleId;
|
||||||
|
|
||||||
|
// Formular mit aktuellen Werten füllen
|
||||||
|
document.getElementById('editDescription').value = this.dataset.description;
|
||||||
|
document.getElementById('editAmount').value = this.dataset.amount;
|
||||||
|
document.getElementById('editType').value = this.dataset.type;
|
||||||
|
document.getElementById('editInterval').value = this.dataset.interval;
|
||||||
|
document.getElementById('editStartDate').value = this.dataset.startDate;
|
||||||
|
document.getElementById('editEndDate').value = this.dataset.endDate || '';
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel Button
|
||||||
|
cancelBtn.addEventListener('click', function() {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form Submit
|
||||||
|
editForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
description: document.getElementById('editDescription').value,
|
||||||
|
amount: parseFloat(document.getElementById('editAmount').value),
|
||||||
|
type: document.getElementById('editType').value,
|
||||||
|
interval: document.getElementById('editInterval').value,
|
||||||
|
start_date: document.getElementById('editStartDate').value,
|
||||||
|
end_date: document.getElementById('editEndDate').value
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/recurring-rules/${currentRuleId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern: ' + (data.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Fehler: ' + error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle Button Event Listener
|
||||||
|
document.querySelectorAll('.toggle-rule-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const ruleId = this.dataset.ruleId;
|
||||||
|
|
||||||
|
fetch(`/transactions/recurring/${ruleId}/toggle`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete Button Event Listener
|
||||||
|
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (confirm('Sind Sie sicher, dass Sie diese wiederkehrende Transaktion löschen möchten?')) {
|
||||||
|
const ruleId = this.dataset.ruleId;
|
||||||
|
|
||||||
|
fetch(`/api/recurring-rules/${ruleId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
templ RegisterPage(errorMsg string) {
|
||||||
|
@Layout("Registrieren") {
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
Neues Konto erstellen
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
Oder
|
||||||
|
<a href="/login" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||||
|
mit bestehendem Konto anmelden
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if errorMsg != "" {
|
||||||
|
<div class="rounded-md bg-red-50 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
{ errorMsg }
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form class="mt-8 space-y-6" action="/register" method="POST">
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="sr-only">Name</label>
|
||||||
|
<input id="name" name="name" type="text" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Vollständiger Name"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="sr-only">E-Mail-Adresse</label>
|
||||||
|
<input id="email" name="email" type="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="E-Mail-Adresse"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only">Passwort</label>
|
||||||
|
<input id="password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password_confirm" class="sr-only">Passwort bestätigen</label>
|
||||||
|
<input id="password_confirm" name="password_confirm" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Passwort bestätigen"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
Registrieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.778
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func RegisterPage(errorMsg string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8\"><div class=\"max-w-md w-full space-y-8\"><div><h2 class=\"mt-6 text-center text-3xl font-extrabold text-gray-900\">Neues Konto erstellen</h2><p class=\"mt-2 text-center text-sm text-gray-600\">Oder <a href=\"/login\" class=\"font-medium text-indigo-600 hover:text-indigo-500\">mit bestehendem Konto anmelden</a></p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"rounded-md bg-red-50 p-4\"><div class=\"flex\"><div class=\"flex-shrink-0\"><svg class=\"h-5 w-5 text-red-400\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\"></path></svg></div><div class=\"ml-3\"><h3 class=\"text-sm font-medium text-red-800\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorMsg)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/register.templ`, Line: 29, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h3></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form class=\"mt-8 space-y-6\" action=\"/register\" method=\"POST\"><div class=\"rounded-md shadow-sm -space-y-px\"><div><label for=\"name\" class=\"sr-only\">Name</label> <input id=\"name\" name=\"name\" type=\"text\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Vollständiger Name\"></div><div><label for=\"email\" class=\"sr-only\">E-Mail-Adresse</label> <input id=\"email\" name=\"email\" type=\"email\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"E-Mail-Adresse\"></div><div><label for=\"password\" class=\"sr-only\">Passwort</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort\"></div><div><label for=\"password_confirm\" class=\"sr-only\">Passwort bestätigen</label> <input id=\"password_confirm\" name=\"password_confirm\" type=\"password\" required class=\"appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm\" placeholder=\"Passwort bestätigen\"></div></div><div><button type=\"submit\" class=\"group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\">Registrieren</button></div></form></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Registrieren").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"whereismymoney/internal/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Settings(userName string, user models.User, categories []models.Category, bankAccounts []models.BankAccount) {
|
||||||
|
@Layout("Einstellungen - WhereIsMyMoney") {
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
@Navigation(userName)
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Einstellungen</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Verwalte deine Kontoinformationen und App-Einstellungen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Benutzereinstellungen -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900">Benutzereinstellungen</h2>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<form id="userSettingsForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
|
||||||
|
<input type="text" id="username" name="username" value={ user.Name } class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700">E-Mail</label>
|
||||||
|
<input type="email" id="email" name="email" value={ user.Email } readonly class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500 cursor-not-allowed" title="E-Mail-Adresse kann nicht geändert werden">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Die E-Mail-Adresse dient als eindeutige Benutzer-ID und kann nicht geändert werden.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passwort ändern -->
|
||||||
|
<div class="bg-white shadow rounded-lg mt-6">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900">Passwort ändern</h2>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<form id="passwordForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="current_password" class="block text-sm font-medium text-gray-700">Aktuelles Passwort</label>
|
||||||
|
<input type="password" id="current_password" name="current_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="new_password" class="block text-sm font-medium text-gray-700">Neues Passwort</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="block text-sm font-medium text-gray-700">Passwort bestätigen</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div class="pt-4">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
|
||||||
|
Passwort ändern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Seitenleiste -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Kategorien verwalten -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Kategorien</h3>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="space-y-2 max-h-48 overflow-y-auto">
|
||||||
|
for _, category := range categories {
|
||||||
|
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded">
|
||||||
|
<span class="text-sm">{ category.Icon } { category.Name }</span>
|
||||||
|
<button class="delete-category text-red-600 hover:text-red-800 text-sm" data-category-id={ fmt.Sprintf("%d", category.ID) }>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button onclick="showModal('categoryModal')" class="mt-4 w-full px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 text-sm">
|
||||||
|
+ Neue Kategorie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Konten verwalten -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Bankkonten</h3>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div class="space-y-2 max-h-48 overflow-y-auto">
|
||||||
|
for _, account := range bankAccounts {
|
||||||
|
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="font-medium">{ account.Name }</div>
|
||||||
|
<div class="text-gray-500">{ account.Bank }</div>
|
||||||
|
</div>
|
||||||
|
<button class="delete-account text-red-600 hover:text-red-800 text-sm" data-account-id={ fmt.Sprintf("%d", account.ID) }>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<a href="/accounts" class="mt-4 w-full block px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 text-sm text-center">
|
||||||
|
Konten verwalten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App-Informationen -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">App-Informationen</h3>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 space-y-2 text-sm text-gray-600">
|
||||||
|
<div>Version: 1.0.0</div>
|
||||||
|
<div>Erstellt mit Go & Templ</div>
|
||||||
|
<div>© 2025 WhereIsMyMoney</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie Modal -->
|
||||||
|
<div id="categoryModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<form id="categoryForm" method="POST" action="/settings/categories">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Neue Kategorie erstellen</h3>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="category_name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||||
|
<input type="text" id="category_name" name="name" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="z.B. Lebensmittel">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="category_icon" class="block text-sm font-medium text-gray-700">Icon (Emoji)</label>
|
||||||
|
<input type="text" id="category_icon" name="icon" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="🛒" maxlength="2">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="category_color" class="block text-sm font-medium text-gray-700">Farbe</label>
|
||||||
|
<input type="color" id="category_color" name="color" value="#3B82F6" class="w-full h-10 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideModal('categoryModal')" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners für Delete-Buttons
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Delete Category Buttons
|
||||||
|
document.querySelectorAll('.delete-category').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const categoryId = this.getAttribute('data-category-id');
|
||||||
|
deleteCategory(categoryId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete Account Buttons
|
||||||
|
document.querySelectorAll('.delete-account').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const accountId = this.getAttribute('data-account-id');
|
||||||
|
deleteAccount(accountId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteCategory(categoryId) {
|
||||||
|
if (!confirm('Möchten Sie diese Kategorie wirklich löschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/settings/categories/${categoryId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen der Kategorie');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAccount(accountId) {
|
||||||
|
if (!confirm('Möchten Sie dieses Konto wirklich löschen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/settings/accounts/${accountId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen des Kontos');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benutzereinstellungen speichern
|
||||||
|
document.getElementById('userSettingsForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Nur den Benutzernamen senden, E-Mail wird ignoriert
|
||||||
|
const formObject = {
|
||||||
|
username: document.getElementById('username').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/settings/user', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formObject)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Einstellungen gespeichert!');
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern der Einstellungen');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Fehler beim Speichern der Einstellungen');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Passwort ändern
|
||||||
|
document.getElementById('passwordForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const newPassword = document.getElementById('new_password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('Die Passwörter stimmen nicht überein!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const formObject = {};
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
formObject[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/settings/password', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formObject)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Passwort erfolgreich geändert!');
|
||||||
|
this.reset();
|
||||||
|
} else {
|
||||||
|
const error = await response.text();
|
||||||
|
alert('Fehler: ' + error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Fehler beim Ändern des Passworts');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal schließen beim Klick außerhalb
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
if (event.target.classList.contains('bg-opacity-50')) {
|
||||||
|
event.target.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"whereismymoney/internal/config"
|
||||||
|
"whereismymoney/internal/database"
|
||||||
|
"whereismymoney/internal/handlers"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Fehler beim Laden der Konfiguration:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
db, err := database.Connect(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Fehler beim Verbinden zur Datenbank:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize handlers
|
||||||
|
h := handlers.NewHandler(db)
|
||||||
|
|
||||||
|
// Setup Gin router
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// Statische Dateien servieren
|
||||||
|
r.Static("/static", "./static")
|
||||||
|
|
||||||
|
// Auth Routes
|
||||||
|
r.GET("/login", h.ShowLogin)
|
||||||
|
r.POST("/login", h.Login)
|
||||||
|
r.GET("/register", h.ShowRegister)
|
||||||
|
r.POST("/register", h.Register)
|
||||||
|
r.POST("/logout", h.Logout)
|
||||||
|
|
||||||
|
// Protected Routes
|
||||||
|
r.GET("/", h.Dashboard)
|
||||||
|
r.GET("/api/dashboard-data", h.DashboardDataAPI)
|
||||||
|
r.GET("/accounts", h.ShowAccounts)
|
||||||
|
r.POST("/accounts/bank", h.CreateBankAccount)
|
||||||
|
r.POST("/accounts/depot", h.CreateDepot)
|
||||||
|
|
||||||
|
// Test Route für wiederkehrende Transaktionen
|
||||||
|
r.GET("/test/recurring", h.TestRecurringTransactions)
|
||||||
|
|
||||||
|
// Transaction Routes
|
||||||
|
r.GET("/transactions", h.ShowTransactions)
|
||||||
|
r.POST("/transactions", h.CreateTransaction)
|
||||||
|
r.POST("/transactions/recurring", h.CreateRecurringTransaction)
|
||||||
|
r.DELETE("/transactions/:id", h.DeleteTransaction)
|
||||||
|
r.GET("/transactions/:id/data", h.GetTransactionData)
|
||||||
|
r.PUT("/transactions/:id", h.UpdateTransaction)
|
||||||
|
r.PUT("/transactions/multi-update", h.MultiUpdateTransactions)
|
||||||
|
r.DELETE("/transactions/multi-delete", h.MultiDeleteTransactions)
|
||||||
|
r.POST("/transactions/recurring/:id/toggle", h.ToggleRecurrenceRule)
|
||||||
|
|
||||||
|
// Recurring Transactions Management
|
||||||
|
r.GET("/recurring", h.ShowRecurringTransactions)
|
||||||
|
|
||||||
|
// Recurring Transaction Management Routes
|
||||||
|
r.GET("/api/recurring-rules", h.GetRecurrenceRules)
|
||||||
|
r.PUT("/api/recurring-rules/:id", h.UpdateRecurrenceRule)
|
||||||
|
r.DELETE("/api/recurring-rules/:id", h.DeleteRecurrenceRule)
|
||||||
|
|
||||||
|
// Settings Routes
|
||||||
|
r.GET("/settings", h.ShowSettings)
|
||||||
|
r.PUT("/settings/user", h.UpdateUserSettings)
|
||||||
|
r.PUT("/settings/password", h.UpdatePassword)
|
||||||
|
r.POST("/settings/categories", h.CreateCategory)
|
||||||
|
r.DELETE("/settings/categories/:id", h.DeleteCategory)
|
||||||
|
r.DELETE("/settings/accounts/:id", h.DeleteBankAccount)
|
||||||
|
|
||||||
|
addr := cfg.Server.Host + ":" + cfg.Server.Port
|
||||||
|
log.Printf("Server startet auf %s...", addr)
|
||||||
|
log.Printf("Öffne http://%s in deinem Browser", addr)
|
||||||
|
|
||||||
|
if err := r.Run(":" + cfg.Server.Port); err != nil {
|
||||||
|
log.Fatal("Server konnte nicht gestartet werden:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
// Dashboard Charts JavaScript - Mit echten Daten aus der API
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Dashboard charts initializing...');
|
||||||
|
|
||||||
|
// Prüfe ob Chart.js geladen ist
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
console.error('Chart.js not loaded!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Chart.js loaded, version:', Chart.version);
|
||||||
|
|
||||||
|
// Dashboard-Daten über API laden
|
||||||
|
fetch('/api/dashboard-data')
|
||||||
|
.then(response => {
|
||||||
|
console.log('API Response status:', response.status);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Dashboard data loaded:', data);
|
||||||
|
initializeCharts(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
// Fallback mit Demo-Daten
|
||||||
|
console.log('Using fallback demo data...');
|
||||||
|
initializeChartsWithDemoData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeChartsWithDemoData() {
|
||||||
|
console.log('Initializing charts with demo data...');
|
||||||
|
|
||||||
|
const demoData = {
|
||||||
|
monthly_stats: [
|
||||||
|
{ month: '2024-01', income: 3000, expenses: 2200, net_change: 800, balance: 5000 },
|
||||||
|
{ month: '2024-02', income: 3200, expenses: 2400, net_change: 800, balance: 5800 },
|
||||||
|
{ month: '2024-03', income: 2800, expenses: 2100, net_change: 700, balance: 6500 }
|
||||||
|
],
|
||||||
|
category_stats: [
|
||||||
|
{ category_name: 'Lebensmittel', total_amount: 600, percentage: 27.3 },
|
||||||
|
{ category_name: 'Transport', total_amount: 250, percentage: 11.4 },
|
||||||
|
{ category_name: 'Unterhaltung', total_amount: 180, percentage: 8.2 },
|
||||||
|
{ category_name: 'Sonstiges', total_amount: 170, percentage: 7.7 }
|
||||||
|
],
|
||||||
|
transaction_trend: [
|
||||||
|
{ date: '2024-03-01T00:00:00Z', income: 100, expense: 50, balance: 5000 },
|
||||||
|
{ date: '2024-03-02T00:00:00Z', income: 0, expense: 80, balance: 4920 },
|
||||||
|
{ date: '2024-03-03T00:00:00Z', income: 200, expense: 120, balance: 5000 },
|
||||||
|
{ date: '2024-03-04T00:00:00Z', income: 50, expense: 90, balance: 4960 },
|
||||||
|
{ date: '2024-03-05T00:00:00Z', income: 0, expense: 70, balance: 4890 }
|
||||||
|
],
|
||||||
|
asset_overview: {
|
||||||
|
total_bank_balance: 15000,
|
||||||
|
total_depot_value: 25000
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeCharts(demoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCharts(data) {
|
||||||
|
console.log('Initializing charts with data:', data);
|
||||||
|
|
||||||
|
// Chart 1: Monatliche Statistiken
|
||||||
|
createMonthlyChart(data);
|
||||||
|
|
||||||
|
// Chart 2: Einnahmen vs Ausgaben
|
||||||
|
createIncomeExpenseChart(data);
|
||||||
|
|
||||||
|
// Chart 3: Trend-Chart
|
||||||
|
createTrendChart(data);
|
||||||
|
|
||||||
|
console.log('All charts initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMonthlyChart(data) {
|
||||||
|
const ctx = document.getElementById('monthlyChart');
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn('Monthly chart canvas not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Echte Datenstruktur aus der API verwenden
|
||||||
|
const monthlyStats = data.monthly_stats || data.monthlyStats || [];
|
||||||
|
console.log('Monthly stats data:', monthlyStats);
|
||||||
|
|
||||||
|
new Chart(ctx.getContext('2d'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: monthlyStats.map(d => {
|
||||||
|
// Month aus "2024-01" Format zu "Januar 2024" konvertieren
|
||||||
|
const [year, month] = d.month.split('-');
|
||||||
|
const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||||
|
return `${monthNames[parseInt(month) - 1]} ${year}`;
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Einnahmen',
|
||||||
|
data: monthlyStats.map(d => parseFloat(d.income || 0)),
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.7)',
|
||||||
|
borderColor: 'rgba(34, 197, 94, 1)',
|
||||||
|
borderWidth: 1
|
||||||
|
}, {
|
||||||
|
label: 'Ausgaben',
|
||||||
|
data: monthlyStats.map(d => parseFloat(d.expenses || 0)),
|
||||||
|
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
||||||
|
borderColor: 'rgba(239, 68, 68, 1)',
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return '€' + value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Monatliche Ein- und Ausgaben'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Monthly chart created');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating monthly chart:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIncomeExpenseChart(data) {
|
||||||
|
const ctx = document.getElementById('incomeExpenseChart');
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn('Income/Expense chart canvas not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Echte Datenstruktur verwenden
|
||||||
|
const monthlyStats = data.monthly_stats || data.monthlyStats || [];
|
||||||
|
console.log('Income/Expense stats data:', monthlyStats);
|
||||||
|
|
||||||
|
const totalIncome = monthlyStats.reduce((sum, d) => sum + parseFloat(d.income || 0), 0);
|
||||||
|
const totalExpenses = monthlyStats.reduce((sum, d) => sum + parseFloat(d.expenses || 0), 0);
|
||||||
|
|
||||||
|
new Chart(ctx.getContext('2d'), {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['Einnahmen', 'Ausgaben'],
|
||||||
|
datasets: [{
|
||||||
|
data: [totalIncome, totalExpenses],
|
||||||
|
backgroundColor: ['#22c55e', '#ef4444'],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#ffffff'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Einnahmen vs. Ausgaben'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'bottom'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.label + ': €' + context.parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Income/Expense chart created');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating income/expense chart:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrendChart(data) {
|
||||||
|
const ctx = document.getElementById('trendChart');
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn('Trend chart canvas not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Echte Datenstruktur verwenden
|
||||||
|
const trendData = data.transaction_trend || data.transactionTrend || [];
|
||||||
|
console.log('Trend data:', trendData);
|
||||||
|
|
||||||
|
new Chart(ctx.getContext('2d'), {
|
||||||
|
type: 'bar', // Basis-Typ für Balken
|
||||||
|
data: {
|
||||||
|
labels: trendData.map(d => {
|
||||||
|
// Datum als Monat/Jahr formatieren für 12-Monats-Ansicht
|
||||||
|
const date = new Date(d.date);
|
||||||
|
return date.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' });
|
||||||
|
}),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Einnahmen',
|
||||||
|
type: 'bar', // Explizit als Bar definieren
|
||||||
|
data: trendData.map(d => parseFloat(d.income || 0)),
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.7)',
|
||||||
|
borderColor: 'rgba(34, 197, 94, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
yAxisID: 'y'
|
||||||
|
}, {
|
||||||
|
label: 'Ausgaben',
|
||||||
|
type: 'bar', // Explizit als Bar definieren
|
||||||
|
data: trendData.map(d => parseFloat(d.expense || 0)),
|
||||||
|
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
||||||
|
borderColor: 'rgba(239, 68, 68, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
yAxisID: 'y'
|
||||||
|
}, {
|
||||||
|
label: 'Saldo',
|
||||||
|
type: 'line', // Explizit als Linie definieren
|
||||||
|
data: trendData.map(d => parseFloat(d.balance || 0)),
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
borderColor: 'rgba(59, 130, 246, 1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.1,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Datum'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Einnahmen/Ausgaben (€)'
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return '€' + value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Saldo (€)'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return '€' + value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '90-Tage Trend'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.dataset.label + ': €' + context.parsed.y.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Trend chart created');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating trend chart:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user