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}) }