Files
Matthias Hinrichs 189e7a2329 first commit
2025-08-26 03:17:49 +02:00

1965 lines
55 KiB
Go

package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"whereismymoney/internal/models"
"whereismymoney/internal/views"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type Handler struct {
DB *gorm.DB
Store *sessions.CookieStore
}
func NewHandler(db *gorm.DB) *Handler {
store := sessions.NewCookieStore([]byte("your-secret-key-here"))
// Session-Optionen für Safari-Kompatibilität
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7, // 7 Tage
HttpOnly: true,
Secure: false, // Für localhost Development
SameSite: http.SameSiteLaxMode, // Safari-kompatibel
}
return &Handler{
DB: db,
Store: store,
}
}
// Dashboard zeigt das Hauptdashboard (nur für angemeldete Benutzer)
func (h *Handler) Dashboard(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Benutzer aus der Datenbank laden
var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Testdaten generieren falls nötig
h.generateTestData(userID.(uint))
// Wiederkehrende Transaktionen generieren
h.processRecurringTransactions(userID.(uint))
// Dashboard-Daten sammeln
dashboardData, err := h.generateDashboardData(userID.(uint))
if err != nil {
c.String(http.StatusInternalServerError, "Fehler beim Laden der Dashboard-Daten: %v", err)
return
}
// Dashboard rendern
component := views.Dashboard(dashboardData)
component.Render(c.Request.Context(), c.Writer)
}
// DashboardDataAPI liefert Dashboard-Daten als JSON für AJAX-Anfragen
func (h *Handler) DashboardDataAPI(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Testdaten generieren falls nötig
h.generateTestData(userID.(uint))
// Wiederkehrende Transaktionen generieren
h.processRecurringTransactions(userID.(uint))
// Dashboard-Daten generieren
data, err := h.generateDashboardData(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Laden der Dashboard-Daten"})
return
}
// Daten als JSON zurückgeben
c.JSON(http.StatusOK, data)
}
// generateDashboardData sammelt alle Daten für das Dashboard
func (h *Handler) generateDashboardData(userID uint) (*models.DashboardData, error) {
data := &models.DashboardData{}
// Wiederkehrende Transaktionen verarbeiten BEVOR Dashboard-Daten generiert werden
if err := h.processRecurringTransactions(userID); err != nil {
// Fehler loggen, aber nicht den gesamten Dashboard-Aufruf fehlschlagen lassen
fmt.Printf("Fehler beim Verarbeiten wiederkehrender Transaktionen: %v\n", err)
}
// Benutzer laden
var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
return nil, err
}
data.UserName = user.Name
// Vermögensübersicht generieren
assetOverview, err := h.generateAssetOverview(userID)
if err != nil {
return nil, err
}
data.AssetOverview = *assetOverview
// Monatliche Statistiken generieren
monthlyStats, err := h.generateMonthlyStats(userID)
if err != nil {
return nil, err
}
data.MonthlyStats = monthlyStats
// Kategorie-Statistiken generieren
categoryStats, err := h.generateCategoryStats(userID)
if err != nil {
return nil, err
}
data.CategoryStats = categoryStats
// Transaktionstrends generieren
transactionTrend, err := h.generateTransactionTrend(userID)
if err != nil {
return nil, err
}
data.TransactionTrend = transactionTrend
// Aktuelle Transaktionen laden
recentTransactions, err := h.getRecentTransactions(userID)
if err != nil {
return nil, err
}
data.RecentTransactions = recentTransactions
// Wöchentliche Statistiken generieren
weeklyStats, err := h.generateWeeklyStats(userID)
if err != nil {
return nil, err
}
data.WeeklyStats = weeklyStats
return data, nil
}
// generateAssetOverview erstellt eine Vermögensübersicht
func (h *Handler) generateAssetOverview(userID uint) (*models.AssetOverview, error) {
overview := &models.AssetOverview{}
// Bank-Konten laden
var bankAccounts []models.BankAccount
if err := h.DB.Where("user_id = ?", userID).Find(&bankAccounts).Error; err != nil {
return nil, err
}
totalBankBalance := 0.0
bankSummaries := make([]models.BankAccountSummary, len(bankAccounts))
for i, account := range bankAccounts {
totalBankBalance += account.Balance
bankSummaries[i] = models.BankAccountSummary{
ID: account.ID,
Name: account.Name,
Bank: account.Bank,
AccountType: account.AccountType,
Balance: account.Balance,
}
}
// Depots laden
var depots []models.Depot
if err := h.DB.Where("user_id = ?", userID).Find(&depots).Error; err != nil {
return nil, err
}
totalDepotValue := 0.0
depotSummaries := make([]models.DepotSummary, len(depots))
for i, depot := range depots {
totalDepotValue += depot.TotalValue
depotSummaries[i] = models.DepotSummary{
ID: depot.ID,
Name: depot.Name,
Broker: depot.Broker,
TotalValue: depot.TotalValue,
}
}
overview.TotalBankBalance = totalBankBalance
overview.TotalDepotValue = totalDepotValue
overview.TotalAssets = totalBankBalance + totalDepotValue
overview.BankAccounts = bankSummaries
overview.Depots = depotSummaries
return overview, nil
}
// generateMonthlyStats erstellt monatliche Statistiken
func (h *Handler) generateMonthlyStats(userID uint) ([]models.MonthlyStats, error) {
var stats []models.MonthlyStats
// Letzte 12 Monate
now := time.Now()
for i := 11; i >= 0; i-- {
month := now.AddDate(0, -i, 0)
startOfMonth := time.Date(month.Year(), month.Month(), 1, 0, 0, 0, 0, month.Location())
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Nanosecond)
var income, expenses float64
// Einnahmen berechnen
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date <= ?", userID, "income", startOfMonth, endOfMonth).
Select("COALESCE(SUM(amount), 0)").Scan(&income)
// Ausgaben berechnen
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date <= ?", userID, "expense", startOfMonth, endOfMonth).
Select("COALESCE(SUM(amount), 0)").Scan(&expenses)
netChange := income - expenses
// Balance zu diesem Zeitpunkt (vereinfacht)
var balance float64
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND date <= ?", userID, endOfMonth).
Select("COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE -amount END), 0)").Scan(&balance)
stats = append(stats, models.MonthlyStats{
Month: month.Format("2006-01"),
Income: income,
Expenses: expenses,
NetChange: netChange,
Balance: balance,
})
}
return stats, nil
}
// generateCategoryStats erstellt Kategorie-Statistiken
func (h *Handler) generateCategoryStats(userID uint) ([]models.CategoryStats, error) {
var stats []models.CategoryStats
// Letzte 90 Tage für bessere Relevanz
ninetyDaysAgo := time.Now().AddDate(0, 0, -90)
type categoryResult struct {
CategoryID *uint
CategoryName *string
TotalAmount float64
Count int64
}
var results []categoryResult
query := `
SELECT
t.category_id,
c.name as category_name,
SUM(t.amount) as total_amount,
COUNT(*) as count
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ? AND t.date >= ? AND t.type = 'expense'
GROUP BY t.category_id, c.name
ORDER BY total_amount DESC
`
if err := h.DB.Raw(query, userID, ninetyDaysAgo).Scan(&results).Error; err != nil {
return nil, err
}
// Gesamtsumme für Prozentberechnung
var totalExpenses float64
for _, result := range results {
totalExpenses += result.TotalAmount
}
for _, result := range results {
categoryName := "Ohne Kategorie"
var categoryID uint = 0
if result.CategoryID != nil {
categoryID = *result.CategoryID
}
if result.CategoryName != nil {
categoryName = *result.CategoryName
}
percentage := 0.0
if totalExpenses > 0 {
percentage = (result.TotalAmount / totalExpenses) * 100
}
stats = append(stats, models.CategoryStats{
CategoryID: categoryID,
CategoryName: categoryName,
TotalAmount: result.TotalAmount,
Count: int(result.Count),
Percentage: percentage,
})
}
return stats, nil
}
// generateTransactionTrend erstellt Verlaufsdaten für Charts
func (h *Handler) generateTransactionTrend(userID uint) ([]models.TransactionTrend, error) {
var trends []models.TransactionTrend
// Nächste 12 Monate (für Trend mit wiederkehrenden Transaktionen)
now := time.Now()
// 12 Monate gruppiert nach Monaten
for i := 0; i < 12; i++ {
// Erster Tag des Monats
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, i, 0)
// Letzter Tag des Monats
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
var income, expense float64
// Einnahmen für diesen Monat (mit DATE-Funktion)
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date < ?", userID, "income", monthStart, monthEnd.AddDate(0, 0, 1)).
Select("COALESCE(SUM(amount), 0)").Scan(&income)
// Ausgaben für diesen Monat (mit DATE-Funktion)
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date < ?", userID, "expense", monthStart, monthEnd.AddDate(0, 0, 1)).
Select("COALESCE(SUM(amount), 0)").Scan(&expense)
// Aktueller Kontostand berechnen
var balance float64
if i == 0 {
// Für den ersten Monat: Aktueller Stand aus allen vergangenen Transaktionen
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND date <= ?", userID, monthEnd).
Select("COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE -amount END), 0)").Scan(&balance)
} else {
// Für zukünftige Monate: Vorheriger Balance + heutige Einnahmen - heutige Ausgaben
if len(trends) > 0 {
balance = trends[len(trends)-1].Balance + income - expense
}
}
trends = append(trends, models.TransactionTrend{
Date: monthStart,
Income: income,
Expense: expense,
Balance: balance,
})
}
return trends, nil
}
// getRecentTransactions lädt die neuesten Transaktionen
func (h *Handler) getRecentTransactions(userID uint) ([]models.Transaction, error) {
var transactions []models.Transaction
err := h.DB.Where("user_id = ?", userID).
Preload("Category").
Preload("BankAccount").
Order("date DESC, created_at DESC").
Limit(10).
Find(&transactions).Error
return transactions, err
}
// generateWeeklyStats erstellt wöchentliche Statistiken
func (h *Handler) generateWeeklyStats(userID uint) ([]models.WeeklyStats, error) {
var stats []models.WeeklyStats
// Letzte 4 Wochen
now := time.Now()
for i := 3; i >= 0; i-- {
// Wochenstart (Montag)
weekStart := now.AddDate(0, 0, -7*i)
for weekStart.Weekday() != time.Monday {
weekStart = weekStart.AddDate(0, 0, -1)
}
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, weekStart.Location())
weekEnd := weekStart.AddDate(0, 0, 6).Add(23*time.Hour + 59*time.Minute + 59*time.Second)
var income, expenses float64
// Einnahmen berechnen
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date <= ?", userID, "income", weekStart, weekEnd).
Select("COALESCE(SUM(amount), 0)").Scan(&income)
// Ausgaben berechnen
h.DB.Model(&models.Transaction{}).
Where("user_id = ? AND type = ? AND date >= ? AND date <= ?", userID, "expense", weekStart, weekEnd).
Select("COALESCE(SUM(amount), 0)").Scan(&expenses)
netChange := income - expenses
weekLabel := fmt.Sprintf("KW %d", getWeekNumber(weekStart))
stats = append(stats, models.WeeklyStats{
Week: weekLabel,
Income: income,
Expenses: expenses,
NetChange: netChange,
StartDate: weekStart,
EndDate: weekEnd,
})
}
return stats, nil
}
// getWeekNumber berechnet die Kalenderwoche
func getWeekNumber(date time.Time) int {
_, week := date.ISOWeek()
return week
}
// ShowLogin zeigt die Login-Seite
func (h *Handler) ShowLogin(c *gin.Context) {
errorMsg := ""
switch c.Query("error") {
case "invalid":
errorMsg = "Ungültige E-Mail-Adresse oder Passwort"
case "session":
errorMsg = "Fehler beim Erstellen der Sitzung. Bitte versuchen Sie es erneut."
case "required":
errorMsg = "Bitte melden Sie sich an, um diese Seite zu besuchen"
}
component := views.LoginPage(errorMsg)
component.Render(c.Request.Context(), c.Writer)
}
// ShowRegister zeigt die Registrierungs-Seite
func (h *Handler) ShowRegister(c *gin.Context) {
errorMsg := ""
switch c.Query("error") {
case "password_mismatch":
errorMsg = "Die Passwörter stimmen nicht überein"
case "email_exists":
errorMsg = "Ein Konto mit dieser E-Mail-Adresse existiert bereits"
case "server":
errorMsg = "Ein Server-Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
case "session":
errorMsg = "Fehler beim Erstellen der Sitzung. Bitte versuchen Sie es erneut."
}
component := views.RegisterPage(errorMsg)
component.Render(c.Request.Context(), c.Writer)
}
// Login verarbeitet die Anmeldung
func (h *Handler) Login(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
// Benutzer in der Datenbank finden
var user models.User
if err := h.DB.Where("email = ?", email).First(&user).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/login?error=invalid")
return
}
// Passwort überprüfen
if !user.CheckPassword(password) {
c.Redirect(http.StatusSeeOther, "/login?error=invalid")
return
}
// Session erstellen
session, _ := h.Store.Get(c.Request, "user-session")
session.Values["user_id"] = user.ID
session.Values["user_name"] = user.Name
// Session explizit speichern mit Fehlerbehandlung
if err := session.Save(c.Request, c.Writer); err != nil {
c.Redirect(http.StatusSeeOther, "/login?error=session")
return
}
c.Redirect(http.StatusSeeOther, "/")
}
// Register verarbeitet die Registrierung
func (h *Handler) Register(c *gin.Context) {
name := c.PostForm("name")
email := c.PostForm("email")
password := c.PostForm("password")
passwordConfirm := c.PostForm("password_confirm")
// Passwort-Bestätigung überprüfen
if password != passwordConfirm {
c.Redirect(http.StatusSeeOther, "/register?error=password_mismatch")
return
}
// Neuen Benutzer erstellen
user := models.User{
Name: name,
Email: email,
}
// Passwort hashen
if err := user.HashPassword(password); err != nil {
c.Redirect(http.StatusSeeOther, "/register?error=server")
return
}
// Benutzer in der Datenbank speichern
if err := h.DB.Create(&user).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/register?error=email_exists")
return
}
// Session erstellen
session, _ := h.Store.Get(c.Request, "user-session")
session.Values["user_id"] = user.ID
session.Values["user_name"] = user.Name
// Session explizit speichern mit Fehlerbehandlung
if err := session.Save(c.Request, c.Writer); err != nil {
c.Redirect(http.StatusSeeOther, "/register?error=session")
return
}
c.Redirect(http.StatusSeeOther, "/")
}
// Logout meldet den Benutzer ab
func (h *Handler) Logout(c *gin.Context) {
session, _ := h.Store.Get(c.Request, "user-session")
session.Values["user_id"] = nil
session.Values["user_name"] = nil
session.Options.MaxAge = -1
session.Save(c.Request, c.Writer)
c.Redirect(http.StatusSeeOther, "/login")
}
// ShowAccounts zeigt die Konten- und Depot-Verwaltung
func (h *Handler) ShowAccounts(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
userName, _ := session.Values["user_name"].(string)
// Bankkonten laden
var bankAccounts []models.BankAccount
h.DB.Where("user_id = ?", userID).Find(&bankAccounts)
// Depots laden
var depots []models.Depot
h.DB.Where("user_id = ?", userID).Find(&depots)
// Template rendern
views.Accounts(userName, bankAccounts, depots).Render(c.Request.Context(), c.Writer)
}
// CreateBankAccount erstellt ein neues Bankkonto
func (h *Handler) CreateBankAccount(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Formulardaten lesen
name := c.PostForm("name")
bankName := c.PostForm("bank_name")
accountType := c.PostForm("account_type")
iban := c.PostForm("iban")
// Balance parsen
var balance float64
if balanceStr := c.PostForm("balance"); balanceStr != "" {
if parsed, err := parseFloat(balanceStr); err == nil {
balance = parsed
}
}
// Validierung
if name == "" || bankName == "" || accountType == "" {
c.Redirect(http.StatusSeeOther, "/accounts?error=required_fields")
return
}
// Bankkonto erstellen
bankAccount := models.BankAccount{
UserID: userID.(uint),
Name: name,
Bank: bankName,
AccountType: accountType,
IBAN: iban,
Balance: balance,
}
if err := h.DB.Create(&bankAccount).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/accounts?error=create_failed")
return
}
c.Redirect(http.StatusSeeOther, "/accounts?success=bank_created")
}
// CreateDepot erstellt ein neues Depot
func (h *Handler) CreateDepot(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Formulardaten lesen
name := c.PostForm("name")
brokerName := c.PostForm("broker_name")
depotNumber := c.PostForm("depot_number")
// Total Value parsen
var totalValue float64
if valueStr := c.PostForm("total_value"); valueStr != "" {
if parsed, err := parseFloat(valueStr); err == nil {
totalValue = parsed
}
}
// Validierung
if name == "" || brokerName == "" {
c.Redirect(http.StatusSeeOther, "/accounts?error=required_fields")
return
}
// Depot erstellen
depot := models.Depot{
UserID: userID.(uint),
Name: name,
Broker: brokerName,
DepotNumber: depotNumber,
TotalValue: totalValue,
}
if err := h.DB.Create(&depot).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/accounts?error=create_failed")
return
}
c.Redirect(http.StatusSeeOther, "/accounts?success=depot_created")
}
// parseFloat ist eine Hilfsfunktion zum sicheren Parsen von Float-Werten
func parseFloat(s string) (float64, error) {
if s == "" {
return 0, nil
}
return strconv.ParseFloat(s, 64)
}
// ShowTransactions zeigt die Transaktionsübersicht
func (h *Handler) ShowTransactions(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
userName, _ := session.Values["user_name"].(string)
// Paginierung Parameter
page := 1
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pageSize := 20
offset := (page - 1) * pageSize
// Filter Parameter
dateFrom := c.Query("date_from")
dateTo := c.Query("date_to")
searchDescription := c.Query("search_description")
filterCategory := c.Query("filter_category")
filterAccount := c.Query("filter_account")
filterType := c.Query("filter_type")
amountMin := c.Query("amount_min")
amountMax := c.Query("amount_max")
// Base Query für Transaktionen
query := h.DB.Model(&models.Transaction{}).Where("user_id = ?", userID)
// Filter anwenden
if dateFrom != "" {
if parsedDate, err := time.Parse("2006-01-02", dateFrom); err == nil {
query = query.Where("date >= ?", parsedDate)
}
}
if dateTo != "" {
if parsedDate, err := time.Parse("2006-01-02", dateTo); err == nil {
query = query.Where("date <= ?", parsedDate)
}
}
if searchDescription != "" {
query = query.Where("description LIKE ?", "%"+searchDescription+"%")
}
if filterCategory != "" {
if filterCategory == "none" {
query = query.Where("category_id IS NULL")
} else if categoryID, err := strconv.Atoi(filterCategory); err == nil {
query = query.Where("category_id = ?", categoryID)
}
}
if filterAccount != "" {
if accountID, err := strconv.Atoi(filterAccount); err == nil {
query = query.Where("bank_account_id = ?", accountID)
}
}
if filterType != "" {
query = query.Where("type = ?", filterType)
}
if amountMin != "" {
if minAmount, err := strconv.ParseFloat(amountMin, 64); err == nil {
query = query.Where("amount >= ?", minAmount)
}
}
if amountMax != "" {
if maxAmount, err := strconv.ParseFloat(amountMax, 64); err == nil {
query = query.Where("amount <= ?", maxAmount)
}
}
// Gesamtanzahl der gefilterten Transaktionen für Paginierung
var totalCount int64
query.Count(&totalCount)
totalPages := int((totalCount + int64(pageSize) - 1) / int64(pageSize))
// Transaktionen laden (neueste zuerst, mit Paginierung und Filtern)
var transactions []models.Transaction
query.Preload("Category").
Preload("BankAccount").
Preload("RecurrenceRule").
Order("date DESC").
Limit(pageSize).
Offset(offset).
Find(&transactions)
// Bankkonten für Dropdown laden
var bankAccounts []models.BankAccount
h.DB.Where("user_id = ?", userID).Find(&bankAccounts)
// Kategorien laden
var categories []models.Category
h.DB.Where("user_id = ?", userID).Find(&categories)
// Regelmäßige Transaktionen laden
var recurrenceRules []models.RecurrenceRule
h.DB.Where("user_id = ? AND is_active = ?", userID, true).
Preload("Category").
Preload("BankAccount").
Find(&recurrenceRules)
// Template rendern
views.Transactions(userName, transactions, bankAccounts, categories, recurrenceRules, page, totalPages, totalCount).Render(c.Request.Context(), c.Writer)
}
// CreateTransaction erstellt eine neue Transaktion
func (h *Handler) CreateTransaction(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Formulardaten lesen
description := c.PostForm("description")
amountStr := c.PostForm("amount")
transactionType := c.PostForm("type") // "income" or "expense"
dateStr := c.PostForm("date")
categoryIDStr := c.PostForm("category_id")
bankAccountIDStr := c.PostForm("bank_account_id")
// Validierung
if description == "" || amountStr == "" || transactionType == "" || dateStr == "" {
c.Redirect(http.StatusSeeOther, "/transactions?error=required_fields")
return
}
// Amount parsen
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=invalid_amount")
return
}
// Datum parsen
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=invalid_date")
return
}
// Optional: Category ID parsen
var categoryID *uint
if categoryIDStr != "" && categoryIDStr != "0" {
if parsed, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
id := uint(parsed)
categoryID = &id
}
}
// Optional: Bank Account ID parsen
var bankAccountID *uint
if bankAccountIDStr != "" && bankAccountIDStr != "0" {
if parsed, err := strconv.ParseUint(bankAccountIDStr, 10, 32); err == nil {
id := uint(parsed)
bankAccountID = &id
}
}
// Transaktion erstellen
transaction := models.Transaction{
UserID: userID.(uint),
Amount: amount,
Description: description,
Type: transactionType,
Date: date,
CategoryID: categoryID,
BankAccountID: bankAccountID,
IsRecurring: false,
}
if err := h.DB.Create(&transaction).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=create_failed")
return
}
c.Redirect(http.StatusSeeOther, "/transactions?success=transaction_created")
}
// CreateRecurringTransaction erstellt eine neue regelmäßige Transaktion
func (h *Handler) CreateRecurringTransaction(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Formulardaten lesen
description := c.PostForm("description")
amountStr := c.PostForm("amount")
transactionType := c.PostForm("type")
startDateStr := c.PostForm("start_date")
endDateStr := c.PostForm("end_date")
interval := c.PostForm("interval")
intervalCountStr := c.PostForm("interval_count")
categoryIDStr := c.PostForm("category_id")
bankAccountIDStr := c.PostForm("bank_account_id")
dayOfMonthStr := c.PostForm("day_of_month")
dayOfWeekStr := c.PostForm("day_of_week")
// Validierung
if description == "" || amountStr == "" || transactionType == "" || startDateStr == "" || interval == "" {
c.Redirect(http.StatusSeeOther, "/transactions?error=required_fields")
return
}
// Amount parsen
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=invalid_amount")
return
}
// Start-Datum parsen
startDate, err := time.Parse("2006-01-02", startDateStr)
if err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=invalid_date")
return
}
// End-Datum parsen (optional)
var endDate *time.Time
if endDateStr != "" {
if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
endDate = &parsed
}
}
// Interval Count parsen
intervalCount := 1
if intervalCountStr != "" {
if parsed, err := strconv.Atoi(intervalCountStr); err == nil && parsed > 0 {
intervalCount = parsed
}
}
// Optional: Category ID parsen
var categoryID *uint
if categoryIDStr != "" && categoryIDStr != "0" {
if parsed, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
id := uint(parsed)
categoryID = &id
}
}
// Optional: Bank Account ID parsen
var bankAccountID *uint
if bankAccountIDStr != "" && bankAccountIDStr != "0" {
if parsed, err := strconv.ParseUint(bankAccountIDStr, 10, 32); err == nil {
id := uint(parsed)
bankAccountID = &id
}
}
// Optional: Day of Month parsen
var dayOfMonth *int
if dayOfMonthStr != "" {
if parsed, err := strconv.Atoi(dayOfMonthStr); err == nil && parsed >= 1 && parsed <= 31 {
dayOfMonth = &parsed
}
}
// Optional: Day of Week parsen
var dayOfWeek *int
if dayOfWeekStr != "" {
if parsed, err := strconv.Atoi(dayOfWeekStr); err == nil && parsed >= 0 && parsed <= 6 {
dayOfWeek = &parsed
}
}
// RecurrenceRule erstellen
recurrenceRule := models.RecurrenceRule{
UserID: userID.(uint),
Amount: amount,
Description: description,
Type: transactionType,
CategoryID: categoryID,
BankAccountID: bankAccountID,
Interval: interval,
IntervalCount: intervalCount,
StartDate: startDate,
EndDate: endDate,
DayOfMonth: dayOfMonth,
DayOfWeek: dayOfWeek,
IsActive: true,
}
if err := h.DB.Create(&recurrenceRule).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/transactions?error=create_failed")
return
}
c.Redirect(http.StatusSeeOther, "/transactions?success=recurring_created")
}
// DeleteTransaction löscht eine Transaktion
func (h *Handler) DeleteTransaction(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Transaction ID aus URL Parameter
transactionIDStr := c.Param("id")
transactionID, err := strconv.ParseUint(transactionIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Transaktions-ID"})
return
}
// Transaktion löschen (nur wenn sie dem Benutzer gehört)
result := h.DB.Where("id = ? AND user_id = ?", transactionID, userID).Delete(&models.Transaction{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Löschen"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Transaktion nicht gefunden"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetTransactionData gibt die Daten einer Transaktion für das Edit-Modal zurück
func (h *Handler) GetTransactionData(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Transaction ID aus URL Parameter
transactionIDStr := c.Param("id")
transactionID, err := strconv.ParseUint(transactionIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Transaktions-ID"})
return
}
// Transaktion laden
var transaction models.Transaction
result := h.DB.Where("id = ? AND user_id = ?", transactionID, userID).First(&transaction)
if result.Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Transaktion nicht gefunden"})
return
}
// Daten für JavaScript formatieren
transactionData := gin.H{
"id": transaction.ID,
"description": transaction.Description,
"amount": transaction.Amount,
"type": transaction.Type,
"date": transaction.Date.Format("2006-01-02"),
"category_id": transaction.CategoryID,
"bank_account_id": transaction.BankAccountID,
}
c.JSON(http.StatusOK, transactionData)
}
// UpdateTransaction aktualisiert eine existierende Transaktion
func (h *Handler) UpdateTransaction(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Transaction ID aus URL Parameter
transactionIDStr := c.Param("id")
transactionID, err := strconv.ParseUint(transactionIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Transaktions-ID"})
return
}
// JSON Body parsen
var reqData struct {
Description string `json:"description"`
Amount string `json:"amount"`
Type string `json:"type"`
Date string `json:"date"`
CategoryID string `json:"category_id"`
BankAccountID string `json:"bank_account_id"`
}
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Daten"})
return
}
// Daten validieren und konvertieren
amount, err := strconv.ParseFloat(reqData.Amount, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültiger Betrag"})
return
}
date, err := time.Parse("2006-01-02", reqData.Date)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültiges Datum"})
return
}
bankAccountID, err := strconv.ParseUint(reqData.BankAccountID, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Konto-ID"})
return
}
// Optional: Category ID parsen
var categoryID *uint
if reqData.CategoryID != "" && reqData.CategoryID != "0" {
if parsed, err := strconv.ParseUint(reqData.CategoryID, 10, 32); err == nil {
id := uint(parsed)
categoryID = &id
}
}
// Transaktion laden und aktualisieren
var transaction models.Transaction
result := h.DB.Where("id = ? AND user_id = ?", transactionID, userID).First(&transaction)
if result.Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Transaktion nicht gefunden"})
return
}
// Transaktion aktualisieren
transaction.Description = reqData.Description
transaction.Amount = amount
transaction.Type = reqData.Type
transaction.Date = date
transaction.CategoryID = categoryID
bankID := uint(bankAccountID)
transaction.BankAccountID = &bankID
if err := h.DB.Save(&transaction).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Speichern"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// MultiUpdateTransactions aktualisiert mehrere Transaktionen gleichzeitig
func (h *Handler) MultiUpdateTransactions(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// JSON Request parsen
var reqData struct {
TransactionIDs []string `json:"transaction_ids"`
Description *string `json:"description,omitempty"`
Amount *string `json:"amount,omitempty"`
Type *string `json:"type,omitempty"`
Date *string `json:"date,omitempty"`
CategoryID *string `json:"category_id,omitempty"`
BankAccountID *string `json:"bank_account_id,omitempty"`
}
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Anfrage"})
return
}
if len(reqData.TransactionIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Keine Transaktionen ausgewählt"})
return
}
// Updates vorbereiten
updates := make(map[string]interface{})
if reqData.Description != nil && *reqData.Description != "" {
updates["description"] = *reqData.Description
}
if reqData.Amount != nil && *reqData.Amount != "" {
amount, err := strconv.ParseFloat(*reqData.Amount, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültiger Betrag"})
return
}
updates["amount"] = amount
}
if reqData.Type != nil && *reqData.Type != "" {
if *reqData.Type != "income" && *reqData.Type != "expense" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültiger Transaktionstyp"})
return
}
updates["type"] = *reqData.Type
}
if reqData.Date != nil && *reqData.Date != "" {
date, err := time.Parse("2006-01-02", *reqData.Date)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültiges Datum"})
return
}
updates["date"] = date
}
if reqData.CategoryID != nil {
if *reqData.CategoryID == "null" || *reqData.CategoryID == "" {
updates["category_id"] = nil
} else {
categoryID, err := strconv.ParseUint(*reqData.CategoryID, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Kategorie-ID"})
return
}
updates["category_id"] = uint(categoryID)
}
}
if reqData.BankAccountID != nil {
if *reqData.BankAccountID == "null" || *reqData.BankAccountID == "" {
updates["bank_account_id"] = nil
} else {
bankAccountID, err := strconv.ParseUint(*reqData.BankAccountID, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Konto-ID"})
return
}
bankID := uint(bankAccountID)
updates["bank_account_id"] = &bankID
}
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Keine Änderungen angegeben"})
return
}
// Transaktionen aktualisieren
result := h.DB.Model(&models.Transaction{}).
Where("id IN ? AND user_id = ?", reqData.TransactionIDs, userID).
Updates(updates)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Aktualisieren der Transaktionen"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"updated": result.RowsAffected,
})
}
// MultiDeleteTransactions löscht mehrere Transaktionen gleichzeitig
func (h *Handler) MultiDeleteTransactions(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// JSON Request parsen
var reqData struct {
TransactionIDs []string `json:"transaction_ids"`
}
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Anfrage"})
return
}
if len(reqData.TransactionIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Keine Transaktionen ausgewählt"})
return
}
// Transaktionen löschen (nur die des angemeldeten Users)
result := h.DB.Where("id IN ? AND user_id = ?", reqData.TransactionIDs, userID).Delete(&models.Transaction{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Löschen der Transaktionen"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"deleted": result.RowsAffected,
})
}
// ToggleRecurrenceRule aktiviert/deaktiviert eine regelmäßige Transaktion
func (h *Handler) ToggleRecurrenceRule(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// RecurrenceRule ID aus URL Parameter
ruleIDStr := c.Param("id")
ruleID, err := strconv.ParseUint(ruleIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Regel-ID"})
return
}
// Regel finden und Status umschalten
var rule models.RecurrenceRule
if err := h.DB.Where("id = ? AND user_id = ?", ruleID, userID).First(&rule).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Regel nicht gefunden"})
return
}
rule.IsActive = !rule.IsActive
if err := h.DB.Save(&rule).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Speichern"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "is_active": rule.IsActive})
}
// generateTestData erstellt Testdaten für ein leeres Dashboard
func (h *Handler) generateTestData(userID uint) error {
// Prüfen, ob bereits Transaktionen vorhanden sind
var count int64
h.DB.Model(&models.Transaction{}).Where("user_id = ?", userID).Count(&count)
if count > 0 {
return nil // Bereits Daten vorhanden
}
// Prüfen, ob bereits wiederkehrende Regeln vorhanden sind
var ruleCount int64
h.DB.Model(&models.RecurrenceRule{}).Where("user_id = ?", userID).Count(&ruleCount)
if ruleCount > 0 {
return nil // Bereits Regeln vorhanden
}
// Beispielkategorien erstellen
categories := []models.Category{
{UserID: userID, Name: "Gehalt", Description: "Monatliches Gehalt", Color: "#22c55e", Icon: "💰"},
{UserID: userID, Name: "Lebensmittel", Description: "Einkäufe und Groceries", Color: "#f59e0b", Icon: "🛒"},
{UserID: userID, Name: "Transport", Description: "ÖPNV, Benzin, etc.", Color: "#3b82f6", Icon: "🚗"},
{UserID: userID, Name: "Unterhaltung", Description: "Kino, Restaurant, etc.", Color: "#8b5cf6", Icon: "🎬"},
{UserID: userID, Name: "Miete", Description: "Wohnungsmiete", Color: "#ef4444", Icon: "🏠"},
}
for _, category := range categories {
if err := h.DB.Create(&category).Error; err != nil {
return err
}
}
// Testdaten für die letzten 3 Monate generieren
now := time.Now()
transactions := []models.Transaction{}
for i := 0; i < 90; i++ { // 90 Tage
date := now.AddDate(0, 0, -i)
// Gehalt am ersten des Monats
if date.Day() == 1 {
transactions = append(transactions, models.Transaction{
UserID: userID,
Amount: 3500.0,
Description: "Gehalt",
Type: "income",
Date: date,
CategoryID: &categories[0].ID,
})
}
// Miete am 1. des Monats
if date.Day() == 1 {
transactions = append(transactions, models.Transaction{
UserID: userID,
Amount: 1200.0,
Description: "Wohnungsmiete",
Type: "expense",
Date: date,
CategoryID: &categories[4].ID,
})
}
// Zufällige tägliche Ausgaben
if i%3 == 0 { // Jeden 3. Tag
// Lebensmittel
transactions = append(transactions, models.Transaction{
UserID: userID,
Amount: float64(15 + (i%50)),
Description: "Einkauf Supermarkt",
Type: "expense",
Date: date,
CategoryID: &categories[1].ID,
})
}
if i%7 == 0 { // Wöchentlich
// Transport
transactions = append(transactions, models.Transaction{
UserID: userID,
Amount: float64(25 + (i%30)),
Description: "ÖPNV Ticket",
Type: "expense",
Date: date,
CategoryID: &categories[2].ID,
})
}
if i%10 == 0 { // Alle 10 Tage
// Unterhaltung
transactions = append(transactions, models.Transaction{
UserID: userID,
Amount: float64(40 + (i%60)),
Description: "Restaurant / Kino",
Type: "expense",
Date: date,
CategoryID: &categories[3].ID,
})
}
}
// Alle Transaktionen in die Datenbank speichern
for _, transaction := range transactions {
if err := h.DB.Create(&transaction).Error; err != nil {
return err
}
}
// Wiederkehrende Transaktionen erstellen
rules := []models.RecurrenceRule{
{
UserID: userID,
Amount: 3500.0,
Description: "Monatliches Gehalt",
CategoryID: &categories[0].ID,
Type: "income",
Interval: "monthly",
IntervalCount: 1,
StartDate: time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()),
DayOfMonth: intPtr(1),
IsActive: true,
},
{
UserID: userID,
Amount: 1200.0,
Description: "Wohnungsmiete",
CategoryID: &categories[4].ID,
Type: "expense",
Interval: "monthly",
IntervalCount: 1,
StartDate: time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()),
DayOfMonth: intPtr(1),
IsActive: true,
},
{
UserID: userID,
Amount: 80.0,
Description: "Wöchentlicher Einkauf",
CategoryID: &categories[1].ID,
Type: "expense",
Interval: "weekly",
IntervalCount: 1,
StartDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()),
DayOfWeek: intPtr(6), // Samstag
IsActive: true,
},
}
// Wiederkehrende Regeln speichern
for _, rule := range rules {
if err := h.DB.Create(&rule).Error; err != nil {
return err
}
}
return nil
}
// intPtr gibt einen Pointer auf einen int zurück
func intPtr(i int) *int {
return &i
}
// TestRecurringTransactions ist eine Test-Route für wiederkehrende Transaktionen
func (h *Handler) TestRecurringTransactions(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Wiederkehrende Transaktionen verarbeiten
err := h.processRecurringTransactions(userID.(uint))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Aktuelle Transaktionen abfragen
var transactions []models.Transaction
h.DB.Where("user_id = ? AND is_recurring = ?", userID, true).
Preload("Category").
Order("date DESC").
Limit(20).
Find(&transactions)
c.JSON(http.StatusOK, gin.H{
"message": "Wiederkehrende Transaktionen verarbeitet",
"recurring_transactions": transactions,
})
}
// UpdateRecurrenceRule aktualisiert eine wiederkehrende Transaktion
func (h *Handler) UpdateRecurrenceRule(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// RecurrenceRule ID aus URL Parameter
ruleIDStr := c.Param("id")
ruleID, err := strconv.ParseUint(ruleIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Regel-ID"})
return
}
// Existierende Regel laden
var rule models.RecurrenceRule
if err := h.DB.Where("id = ? AND user_id = ?", ruleID, userID).First(&rule).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Regel nicht gefunden"})
return
}
// Request Body parsen
var updateData struct {
Amount *float64 `json:"amount"`
Description *string `json:"description"`
CategoryID *uint `json:"category_id"`
Type *string `json:"type"`
Interval *string `json:"interval"`
IntervalCount *int `json:"interval_count"`
DayOfWeek *int `json:"day_of_week"`
DayOfMonth *int `json:"day_of_month"`
IsActive *bool `json:"is_active"`
StartDate *string `json:"start_date"`
EndDate *string `json:"end_date"`
}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Daten"})
return
}
// Felder aktualisieren (nur wenn sie gesetzt sind)
if updateData.Amount != nil {
rule.Amount = *updateData.Amount
}
if updateData.Description != nil {
rule.Description = *updateData.Description
}
if updateData.CategoryID != nil {
rule.CategoryID = updateData.CategoryID
}
if updateData.Type != nil {
rule.Type = *updateData.Type
}
if updateData.Interval != nil {
rule.Interval = *updateData.Interval
}
if updateData.IntervalCount != nil {
rule.IntervalCount = *updateData.IntervalCount
}
if updateData.DayOfWeek != nil {
rule.DayOfWeek = updateData.DayOfWeek
}
if updateData.DayOfMonth != nil {
rule.DayOfMonth = updateData.DayOfMonth
}
if updateData.IsActive != nil {
rule.IsActive = *updateData.IsActive
}
if updateData.StartDate != nil {
if startDate, err := time.Parse("2006-01-02", *updateData.StartDate); err == nil {
rule.StartDate = startDate
}
}
if updateData.EndDate != nil {
if *updateData.EndDate == "" {
rule.EndDate = nil
} else {
if endDate, err := time.Parse("2006-01-02", *updateData.EndDate); err == nil {
rule.EndDate = &endDate
}
}
}
// Regel speichern
if err := h.DB.Save(&rule).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Speichern"})
return
}
// Aktualisierte Regel mit Kategorie laden
h.DB.Preload("Category").First(&rule, rule.ID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"rule": rule,
})
}
// GetRecurrenceRules lädt alle wiederkehrenden Transaktionen eines Benutzers
func (h *Handler) GetRecurrenceRules(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Alle Regeln des Benutzers laden
var rules []models.RecurrenceRule
if err := h.DB.Where("user_id = ?", userID).
Preload("Category").
Order("created_at DESC").
Find(&rules).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Laden der Regeln"})
return
}
c.JSON(http.StatusOK, gin.H{
"rules": rules,
})
}
// DeleteRecurrenceRule löscht eine wiederkehrende Transaktion
func (h *Handler) DeleteRecurrenceRule(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// RecurrenceRule ID aus URL Parameter
ruleIDStr := c.Param("id")
ruleID, err := strconv.ParseUint(ruleIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Regel-ID"})
return
}
// Regel löschen (nur wenn sie dem Benutzer gehört)
result := h.DB.Where("id = ? AND user_id = ?", ruleID, userID).Delete(&models.RecurrenceRule{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Löschen"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Regel nicht gefunden"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// processRecurringTransactions generiert alle fälligen wiederkehrenden Transaktionen
func (h *Handler) processRecurringTransactions(userID uint) error {
// Alle aktiven Wiederholungsregeln für den Benutzer laden
var rules []models.RecurrenceRule
if err := h.DB.Where("user_id = ? AND is_active = ?", userID, true).Find(&rules).Error; err != nil {
return err
}
// Für jede Regel prüfen, ob neue Transaktionen generiert werden müssen
for _, rule := range rules {
// Bis zu 10 Transaktionen pro Regel generieren (um Endlosschleifen zu vermeiden)
for i := 0; i < 10; i++ {
transaction, err := rule.GenerateTransaction(h.DB)
if err != nil {
// Fehler loggen, aber weitermachen
fmt.Printf("Fehler beim Generieren einer wiederkehrenden Transaktion (Regel ID %d): %v\n", rule.ID, err)
break
}
if transaction == nil {
// Keine weitere Transaktion zu generieren
break
}
fmt.Printf("Wiederkehrende Transaktion generiert: %s (€%.2f) für %s\n",
transaction.Description, transaction.Amount, transaction.Date.Format("02.01.2006"))
}
}
return nil
}
// ShowRecurringTransactions zeigt die Verwaltungsseite für wiederkehrende Transaktionen
func (h *Handler) ShowRecurringTransactions(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login?error=required")
return
}
// Benutzer aus der Datenbank laden
var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/login?error=user_not_found")
return
}
// Alle Regeln des Benutzers laden
var rules []models.RecurrenceRule
if err := h.DB.Where("user_id = ?", userID).
Preload("Category").
Order("created_at DESC").
Find(&rules).Error; err != nil {
c.String(http.StatusInternalServerError, "Fehler beim Laden der wiederkehrenden Transaktionen")
return
}
// Template rendern
component := views.RecurringTransactions(user.Name, rules)
component.Render(c.Request.Context(), c.Writer)
}
// ShowSettings zeigt die Einstellungsseite an
func (h *Handler) ShowSettings(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login")
return
}
// Benutzer laden
var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
c.String(http.StatusInternalServerError, "Fehler beim Laden der Benutzerdaten")
return
}
// Kategorien laden
var categories []models.Category
h.DB.Where("user_id = ?", userID).Find(&categories)
// Bankkonten laden
var bankAccounts []models.BankAccount
h.DB.Where("user_id = ?", userID).Find(&bankAccounts)
// Template rendern
views.Settings(user.Name, user, categories, bankAccounts).Render(c.Request.Context(), c.Writer)
}
// UpdateUserSettings aktualisiert die Benutzereinstellungen
func (h *Handler) UpdateUserSettings(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// JSON Body parsen
var reqData struct {
Username string `json:"username"`
}
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Daten"})
return
}
// Benutzer aktualisieren (nur Name, E-Mail bleibt unverändert)
result := h.DB.Model(&models.User{}).Where("id = ?", userID).Update("name", reqData.Username)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Speichern"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UpdatePassword ändert das Benutzerpasswort
func (h *Handler) UpdatePassword(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// JSON Body parsen
var reqData struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Daten"})
return
}
// Benutzer laden
var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Laden der Benutzerdaten"})
return
}
// Aktuelles Passwort überprüfen
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(reqData.CurrentPassword)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Aktuelles Passwort ist falsch"})
return
}
// Neues Passwort hashen
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(reqData.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Hashen des Passworts"})
return
}
// Passwort aktualisieren
result := h.DB.Model(&user).Update("password_hash", string(hashedPassword))
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Speichern des Passworts"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// CreateCategory erstellt eine neue Kategorie
func (h *Handler) CreateCategory(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.Redirect(http.StatusSeeOther, "/login")
return
}
// Form-Daten lesen
name := c.PostForm("name")
icon := c.PostForm("icon")
color := c.PostForm("color")
if name == "" || icon == "" {
c.Redirect(http.StatusSeeOther, "/settings?error=invalid_data")
return
}
// Kategorie erstellen
category := models.Category{
Name: name,
Icon: icon,
Color: color,
UserID: userID.(uint),
}
if err := h.DB.Create(&category).Error; err != nil {
c.Redirect(http.StatusSeeOther, "/settings?error=create_failed")
return
}
c.Redirect(http.StatusSeeOther, "/settings")
}
// DeleteCategory löscht eine Kategorie
func (h *Handler) DeleteCategory(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Category ID aus URL Parameter
categoryIDStr := c.Param("id")
categoryID, err := strconv.ParseUint(categoryIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Kategorie-ID"})
return
}
// Kategorie löschen (nur wenn sie dem Benutzer gehört)
result := h.DB.Where("id = ? AND user_id = ?", categoryID, userID).Delete(&models.Category{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Löschen"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Kategorie nicht gefunden"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DeleteBankAccount löscht ein Bankkonto
func (h *Handler) DeleteBankAccount(c *gin.Context) {
// Session überprüfen
session, _ := h.Store.Get(c.Request, "user-session")
userID, ok := session.Values["user_id"]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Nicht angemeldet"})
return
}
// Account ID aus URL Parameter
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Konto-ID"})
return
}
// Konto löschen (nur wenn es dem Benutzer gehört)
result := h.DB.Where("id = ? AND user_id = ?", accountID, userID).Delete(&models.BankAccount{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Fehler beim Löschen"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Konto nicht gefunden"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}