first commit
This commit is contained in:
@@ -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
Reference in New Issue
Block a user