package controllers import ( "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "fotbal-club/internal/models" ) type FinancialController struct { db *gorm.DB } func NewFinancialController(db *gorm.DB) *FinancialController { return &FinancialController{db: db} } // Budget Management // GetBudgets retrieves all budgets with optional filtering func (fc *FinancialController) GetBudgets(c *gin.Context) { var budgets []models.Budget query := fc.db.Preload("Expenses") // Filter by fiscal year if provided if fiscalYear := c.Query("fiscal_year"); fiscalYear != "" { if year, err := strconv.Atoi(fiscalYear); err == nil { query = query.Where("fiscal_year = ?", year) } } // Filter by category if provided if category := c.Query("category"); category != "" { query = query.Where("category = ?", category) } // Filter by active status if provided if active := c.Query("active"); active != "" { if isActive, err := strconv.ParseBool(active); err == nil { query = query.Where("active = ?", isActive) } } if err := query.Order("category, name").Find(&budgets).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve budgets"}) return } // Calculate budget statistics for each budget for i := range budgets { budgets[i].CurrentSpend = fc.calculateBudgetSpend(budgets[i].ID) } c.JSON(http.StatusOK, budgets) } // GetBudget retrieves a single budget by ID func (fc *FinancialController) GetBudget(c *gin.Context) { id := c.Param("id") var budget models.Budget if err := fc.db.Preload("Expenses").First(&budget, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Budget not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve budget"}) return } // Calculate current spend budget.CurrentSpend = fc.calculateBudgetSpend(budget.ID) c.JSON(http.StatusOK, budget) } // CreateBudget creates a new budget func (fc *FinancialController) CreateBudget(c *gin.Context) { var budget models.Budget if err := c.ShouldBindJSON(&budget); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set default values if budget.FiscalYear == 0 { budget.FiscalYear = time.Now().Year() } if budget.StartDate.IsZero() { budget.StartDate = time.Date(budget.FiscalYear, 1, 1, 0, 0, 0, 0, time.UTC) } if budget.EndDate.IsZero() { budget.EndDate = time.Date(budget.FiscalYear, 12, 31, 23, 59, 59, 0, time.UTC) } // Get current user ID from context (set by auth middleware) userID, exists := c.Get("user_id") if exists { budget.CreatedBy = userID.(uint) } if err := fc.db.Create(&budget).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create budget"}) return } c.JSON(http.StatusCreated, budget) } // UpdateBudget updates an existing budget func (fc *FinancialController) UpdateBudget(c *gin.Context) { id := c.Param("id") var budget models.Budget if err := fc.db.First(&budget, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Budget not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve budget"}) return } var updateData models.Budget if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields budget.Name = updateData.Name budget.Description = updateData.Description budget.Category = updateData.Category budget.YearlyLimit = updateData.YearlyLimit budget.MonthlyLimit = updateData.MonthlyLimit budget.FiscalYear = updateData.FiscalYear budget.StartDate = updateData.StartDate budget.EndDate = updateData.EndDate budget.Active = updateData.Active budget.AlertThreshold = updateData.AlertThreshold // Get current user ID from context userID, exists := c.Get("user_id") if exists { budget.UpdatedBy = userID.(uint) } if err := fc.db.Save(&budget).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update budget"}) return } // Recalculate current spend budget.CurrentSpend = fc.calculateBudgetSpend(budget.ID) c.JSON(http.StatusOK, budget) } // DeleteBudget deletes a budget func (fc *FinancialController) DeleteBudget(c *gin.Context) { id := c.Param("id") // Check if budget has expenses var expenseCount int64 fc.db.Model(&models.Expense{}).Where("budget_id = ?", id).Count(&expenseCount) if expenseCount > 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete budget with associated expenses"}) return } if err := fc.db.Delete(&models.Budget{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete budget"}) return } c.JSON(http.StatusOK, gin.H{"message": "Budget deleted successfully"}) } // GetBudgetCategories retrieves all unique budget categories func (fc *FinancialController) GetBudgetCategories(c *gin.Context) { var categories []string if err := fc.db.Model(&models.Budget{}).Distinct("category").Pluck("category", &categories).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve categories"}) return } c.JSON(http.StatusOK, categories) } // GetBudgetOverview provides budget summary and statistics func (fc *FinancialController) GetBudgetOverview(c *gin.Context) { fiscalYear := c.Query("fiscal_year") if fiscalYear == "" { fiscalYear = strconv.Itoa(time.Now().Year()) } year, err := strconv.Atoi(fiscalYear) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid fiscal year"}) return } var overview struct { TotalBudget float64 `json:"total_budget"` TotalSpend float64 `json:"total_spend"` RemainingBudget float64 `json:"remaining_budget"` BudgetCount int `json:"budget_count"` OverBudgetCount int `json:"over_budget_count"` AlertBudgetCount int `json:"alert_budget_count"` Categories []CategorySummary `json:"categories"` } // Get all budgets for the fiscal year var budgets []models.Budget if err := fc.db.Where("fiscal_year = ? AND active = ?", year, true).Find(&budgets).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve budgets"}) return } // Calculate totals and category summaries categoryMap := make(map[string]*CategorySummary) for _, budget := range budgets { currentSpend := fc.calculateBudgetSpend(budget.ID) overview.TotalBudget += budget.YearlyLimit overview.TotalSpend += currentSpend if currentSpend > budget.YearlyLimit { overview.OverBudgetCount++ } else if currentSpend > (budget.YearlyLimit * budget.AlertThreshold / 100) { overview.AlertBudgetCount++ } // Category summary if _, exists := categoryMap[budget.Category]; !exists { categoryMap[budget.Category] = &CategorySummary{ Category: budget.Category, BudgetTotal: 0, SpendTotal: 0, BudgetCount: 0, } } cat := categoryMap[budget.Category] cat.BudgetTotal += budget.YearlyLimit cat.SpendTotal += currentSpend cat.BudgetCount++ } overview.RemainingBudget = overview.TotalBudget - overview.TotalSpend overview.BudgetCount = len(budgets) // Convert category map to slice for _, cat := range categoryMap { overview.Categories = append(overview.Categories, *cat) } c.JSON(http.StatusOK, overview) } // Helper functions func (fc *FinancialController) calculateBudgetSpend(budgetID uint) float64 { var totalSpend float64 fc.db.Model(&models.Expense{}).Where("budget_id = ? AND status IN ?", budgetID, []string{"approved", "reimbursed"}).Select("COALESCE(SUM(total_amount), 0)").Scan(&totalSpend) return totalSpend } // CategorySummary represents budget summary by category type CategorySummary struct { Category string `json:"category"` BudgetTotal float64 `json:"budget_total"` SpendTotal float64 `json:"spend_total"` BudgetCount int `json:"budget_count"` } // Sponsorship Management // GetSponsorships retrieves all sponsorships with filtering func (fc *FinancialController) GetSponsorships(c *gin.Context) { var sponsorships []models.Sponsorship query := fc.db.Preload("Payments").Preload("Documents") // Filter by status if provided if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } // Filter by contract type if provided if contractType := c.Query("contract_type"); contractType != "" { query = query.Where("contract_type = ?", contractType) } // Filter by active sponsorships if active := c.Query("active"); active == "true" { query = query.Where("status = ? AND end_date >= ?", "active", time.Now()) } if err := query.Order("sponsor_name").Find(&sponsorships).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve sponsorships"}) return } c.JSON(http.StatusOK, sponsorships) } // GetSponsorship retrieves a single sponsorship by ID func (fc *FinancialController) GetSponsorship(c *gin.Context) { id := c.Param("id") var sponsorship models.Sponsorship if err := fc.db.Preload("Payments").Preload("Documents").First(&sponsorship, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Sponsorship not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve sponsorship"}) return } c.JSON(http.StatusOK, sponsorship) } // CreateSponsorship creates a new sponsorship func (fc *FinancialController) CreateSponsorship(c *gin.Context) { var sponsorship models.Sponsorship if err := c.ShouldBindJSON(&sponsorship); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set default values if sponsorship.Status == "" { sponsorship.Status = "active" } if sponsorship.Currency == "" { sponsorship.Currency = "CZK" } // Get current user ID from context userID, exists := c.Get("user_id") if exists { sponsorship.CreatedBy = userID.(uint) } if err := fc.db.Create(&sponsorship).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create sponsorship"}) return } c.JSON(http.StatusCreated, sponsorship) } // UpdateSponsorship updates an existing sponsorship func (fc *FinancialController) UpdateSponsorship(c *gin.Context) { id := c.Param("id") var sponsorship models.Sponsorship if err := fc.db.First(&sponsorship, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Sponsorship not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve sponsorship"}) return } var updateData models.Sponsorship if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields sponsorship.SponsorName = updateData.SponsorName sponsorship.SponsorLogo = updateData.SponsorLogo sponsorship.ContactPerson = updateData.ContactPerson sponsorship.ContactEmail = updateData.ContactEmail sponsorship.ContactPhone = updateData.ContactPhone sponsorship.ContractNumber = updateData.ContractNumber sponsorship.ContractType = updateData.ContractType sponsorship.TotalValue = updateData.TotalValue sponsorship.PaymentSchedule = updateData.PaymentSchedule sponsorship.Currency = updateData.Currency sponsorship.StartDate = updateData.StartDate sponsorship.EndDate = updateData.EndDate sponsorship.AutoRenewal = updateData.AutoRenewal sponsorship.RenewalNotice = updateData.RenewalNotice sponsorship.Benefits = updateData.Benefits sponsorship.Obligations = updateData.Obligations sponsorship.Status = updateData.Status sponsorship.LastPaymentDate = updateData.LastPaymentDate sponsorship.NextPaymentDate = updateData.NextPaymentDate // Get current user ID from context userID, exists := c.Get("user_id") if exists { sponsorship.UpdatedBy = userID.(uint) } if err := fc.db.Save(&sponsorship).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update sponsorship"}) return } c.JSON(http.StatusOK, sponsorship) } // DeleteSponsorship deletes a sponsorship func (fc *FinancialController) DeleteSponsorship(c *gin.Context) { id := c.Param("id") if err := fc.db.Delete(&models.Sponsorship{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete sponsorship"}) return } c.JSON(http.StatusOK, gin.H{"message": "Sponsorship deleted successfully"}) } // GetSponsorshipOverview provides sponsorship summary func (fc *FinancialController) GetSponsorshipOverview(c *gin.Context) { var overview struct { TotalSponsors int64 `json:"total_sponsors"` ActiveSponsors int64 `json:"active_sponsors"` TotalValue float64 `json:"total_value"` ReceivedThisYear float64 `json:"received_this_year"` PendingThisYear float64 `json:"pending_this_year"` ExpiringNextMonth int64 `json:"expiring_next_month"` ContractTypes []ContractTypeSummary `json:"contract_types"` } currentYear := time.Now().Year() nextMonth := time.Now().AddDate(0, 1, 0) // Count sponsors fc.db.Model(&models.Sponsorship{}).Count(&overview.TotalSponsors) fc.db.Model(&models.Sponsorship{}).Where("status = ? AND end_date >= ?", "active", time.Now()).Count(&overview.ActiveSponsors) // Calculate total value fc.db.Model(&models.Sponsorship{}).Where("status = ?", "active").Select("COALESCE(SUM(total_value), 0)").Scan(&overview.TotalValue) // Calculate payments this year fc.db.Model(&models.SponsorshipPayment{}). Where("EXTRACT(YEAR FROM payment_date) = ? AND status = ?", currentYear, "received"). Select("COALESCE(SUM(amount), 0)").Scan(&overview.ReceivedThisYear) fc.db.Model(&models.SponsorshipPayment{}). Where("EXTRACT(YEAR FROM payment_date) = ? AND status = ?", currentYear, "expected"). Select("COALESCE(SUM(amount), 0)").Scan(&overview.PendingThisYear) // Count expiring sponsorships fc.db.Model(&models.Sponsorship{}). Where("status = ? AND end_date <= ? AND end_date >= ?", "active", nextMonth, time.Now()). Count(&overview.ExpiringNextMonth) // Contract type summary var contractTypes []string fc.db.Model(&models.Sponsorship{}).Distinct("contract_type").Pluck("contract_type", &contractTypes) for _, contractType := range contractTypes { var count int64 var totalValue float64 fc.db.Model(&models.Sponsorship{}).Where("contract_type = ? AND status = ?", contractType, "active").Count(&count) fc.db.Model(&models.Sponsorship{}).Where("contract_type = ? AND status = ?", contractType, "active").Select("COALESCE(SUM(total_value), 0)").Scan(&totalValue) overview.ContractTypes = append(overview.ContractTypes, ContractTypeSummary{ Type: contractType, Count: int(count), TotalValue: totalValue, }) } c.JSON(http.StatusOK, overview) } type ContractTypeSummary struct { Type string `json:"type"` Count int `json:"count"` TotalValue float64 `json:"total_value"` } // Expense Management // GetExpenses retrieves all expenses with filtering and pagination func (fc *FinancialController) GetExpenses(c *gin.Context) { var expenses []models.Expense query := fc.db.Preload("Budget").Preload("Documents") // Parse pagination parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset := (page - 1) * limit // Filter by status if provided if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } // Filter by category if provided if category := c.Query("category"); category != "" { query = query.Where("category = ?", category) } // Filter by budget if provided if budgetID := c.Query("budget_id"); budgetID != "" { query = query.Where("budget_id = ?", budgetID) } // Filter by date range if provided if startDate := c.Query("start_date"); startDate != "" { if date, err := time.Parse("2006-01-02", startDate); err == nil { query = query.Where("expense_date >= ?", date) } } if endDate := c.Query("end_date"); endDate != "" { if date, err := time.Parse("2006-01-02", endDate); err == nil { query = query.Where("expense_date <= ?", date) } } // Get total count for pagination var total int64 countQuery := query if err := countQuery.Model(&models.Expense{}).Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count expenses"}) return } // Apply pagination and ordering if err := query.Order("expense_date DESC, created_at DESC").Limit(limit).Offset(offset).Find(&expenses).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expenses"}) return } // Return paginated response c.JSON(http.StatusOK, gin.H{ "expenses": expenses, "total": total, "page": page, "limit": limit, "pages": (total + int64(limit) - 1) / int64(limit), }) } // GetExpense retrieves a single expense by ID func (fc *FinancialController) GetExpense(c *gin.Context) { id := c.Param("id") var expense models.Expense if err := fc.db.Preload("Budget").Preload("Documents").First(&expense, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Expense not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expense"}) return } c.JSON(http.StatusOK, expense) } // CreateExpense creates a new expense func (fc *FinancialController) CreateExpense(c *gin.Context) { var expense models.Expense if err := c.ShouldBindJSON(&expense); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set default values if expense.Currency == "" { expense.Currency = "CZK" } if expense.VATRate == 0 { expense.VATRate = 21 } if expense.Status == "" { expense.Status = "pending" } if expense.ExpenseDate.IsZero() { expense.ExpenseDate = time.Now() } // Calculate VAT and total amounts if not provided if expense.VATAmount == 0 && expense.Amount > 0 { expense.VATAmount = expense.Amount * expense.VATRate / 100 } if expense.TotalAmount == 0 { expense.TotalAmount = expense.Amount + expense.VATAmount } // Get current user ID from context userID, exists := c.Get("user_id") if exists { expense.CreatedBy = userID.(uint) } // Auto-approve if amount is below threshold and approval is not required // This would need to check FinancialSettings, but for now auto-approve amounts <= 1000 if expense.Amount <= 1000 { expense.Status = "approved" now := time.Now() expense.ApprovedAt = &now if userID != nil { expense.ApprovedBy = userID.(uint) } } if err := fc.db.Create(&expense).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create expense"}) return } // Reload with associations if err := fc.db.Preload("Budget").Preload("Documents").First(&expense, expense.ID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve created expense"}) return } c.JSON(http.StatusCreated, expense) } // UpdateExpense updates an existing expense func (fc *FinancialController) UpdateExpense(c *gin.Context) { id := c.Param("id") var expense models.Expense if err := fc.db.First(&expense, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Expense not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expense"}) return } var updateData models.Expense if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Don't allow updating approved/reimbursed expenses unless admin if expense.Status == "approved" || expense.Status == "reimbursed" { // Check if user is admin (simplified check) userRole, _ := c.Get("user_role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Cannot update approved expense"}) return } } // Update fields expense.Title = updateData.Title expense.Description = updateData.Description expense.Category = updateData.Category expense.Subcategory = updateData.Subcategory expense.Amount = updateData.Amount expense.Currency = updateData.Currency expense.VATRate = updateData.VATRate expense.VATAmount = updateData.VATAmount expense.TotalAmount = updateData.TotalAmount expense.ExpenseDate = updateData.ExpenseDate expense.PaymentMethod = updateData.PaymentMethod expense.HasReceipt = updateData.HasReceipt expense.ReceiptData = updateData.ReceiptData expense.ReceiptImage = updateData.ReceiptImage expense.BudgetID = updateData.BudgetID expense.TeamID = updateData.TeamID expense.ProjectID = updateData.ProjectID // Get current user ID from context userID, exists := c.Get("user_id") if exists { expense.UpdatedBy = userID.(uint) } if err := fc.db.Save(&expense).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update expense"}) return } // Reload with associations if err := fc.db.Preload("Budget").Preload("Documents").First(&expense, expense.ID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve updated expense"}) return } c.JSON(http.StatusOK, expense) } // DeleteExpense deletes an expense func (fc *FinancialController) DeleteExpense(c *gin.Context) { id := c.Param("id") var expense models.Expense if err := fc.db.First(&expense, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Expense not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expense"}) return } // Don't allow deleting approved/reimbursed expenses unless admin if expense.Status == "approved" || expense.Status == "reimbursed" { userRole, _ := c.Get("user_role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Cannot delete approved expense"}) return } } if err := fc.db.Delete(&expense).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete expense"}) return } c.JSON(http.StatusOK, gin.H{"message": "Expense deleted successfully"}) } // ApproveExpense approves an expense func (fc *FinancialController) ApproveExpense(c *gin.Context) { id := c.Param("id") var expense models.Expense if err := fc.db.First(&expense, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Expense not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expense"}) return } if expense.Status != "pending" { c.JSON(http.StatusBadRequest, gin.H{"error": "Expense is not pending"}) return } // Get current user ID from context userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } now := time.Now() expense.Status = "approved" expense.ApprovedAt = &now expense.ApprovedBy = userID.(uint) expense.UpdatedBy = userID.(uint) if err := fc.db.Save(&expense).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve expense"}) return } c.JSON(http.StatusOK, expense) } // RejectExpense rejects an expense func (fc *FinancialController) RejectExpense(c *gin.Context) { id := c.Param("id") var request struct { RejectionReason string `json:"rejection_reason" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var expense models.Expense if err := fc.db.First(&expense, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Expense not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve expense"}) return } if expense.Status != "pending" { c.JSON(http.StatusBadRequest, gin.H{"error": "Expense is not pending"}) return } // Get current user ID from context userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } expense.Status = "rejected" expense.RejectionReason = request.RejectionReason expense.UpdatedBy = userID.(uint) if err := fc.db.Save(&expense).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject expense"}) return } c.JSON(http.StatusOK, expense) } // UploadReceipt handles receipt upload for expenses func (fc *FinancialController) UploadReceipt(c *gin.Context) { // This would handle file upload and OCR processing // For now, return a placeholder response c.JSON(http.StatusNotImplemented, gin.H{"error": "Receipt upload not implemented yet"}) } // GetExpenseCategories retrieves all unique expense categories func (fc *FinancialController) GetExpenseCategories(c *gin.Context) { var categories []string if err := fc.db.Model(&models.Expense{}).Distinct("category").Pluck("category", &categories).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve categories"}) return } c.JSON(http.StatusOK, categories) } // GetExpenseOverview provides expense summary and statistics func (fc *FinancialController) GetExpenseOverview(c *gin.Context) { var overview struct { TotalExpenses float64 `json:"total_expenses"` PendingExpenses float64 `json:"pending_expenses"` ApprovedExpenses float64 `json:"approved_expenses"` RejectedExpenses float64 `json:"rejected_expenses"` ReimbursedExpenses float64 `json:"reimbursed_expenses"` ExpenseCount int `json:"expense_count"` ThisMonthExpenses float64 `json:"this_month_expenses"` Categories []CategorySummary `json:"categories"` } // Get current month boundaries now := time.Now() startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) // Calculate totals by status fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "pending").Scan(&overview.PendingExpenses) fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "approved").Scan(&overview.ApprovedExpenses) fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "rejected").Scan(&overview.RejectedExpenses) fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "reimbursed").Scan(&overview.ReimbursedExpenses) // Total expenses overview.TotalExpenses = overview.PendingExpenses + overview.ApprovedExpenses + overview.ReimbursedExpenses // Count all expenses var expenseCount int64 fc.db.Model(&models.Expense{}).Count(&expenseCount) overview.ExpenseCount = int(expenseCount) // This month expenses fc.db.Model(&models.Expense{}).Where("expense_date >= ?", startOfMonth).Select("COALESCE(SUM(total_amount), 0)").Scan(&overview.ThisMonthExpenses) // Category summary var categories []string fc.db.Model(&models.Expense{}).Distinct("category").Pluck("category", &categories) for _, category := range categories { var count int64 var total float64 fc.db.Model(&models.Expense{}).Where("category = ?", category).Count(&count) fc.db.Model(&models.Expense{}).Where("category = ?", category).Select("COALESCE(SUM(total_amount), 0)").Scan(&total) overview.Categories = append(overview.Categories, CategorySummary{ Category: category, BudgetTotal: total, // Using BudgetTotal field for expense total SpendTotal: total, // Same for expenses BudgetCount: int(count), }) } c.JSON(http.StatusOK, overview) } // Financial Dashboard and Settings // GetFinancialDashboard provides comprehensive financial overview func (fc *FinancialController) GetFinancialDashboard(c *gin.Context) { var dashboard struct { // Budget summary BudgetSummary struct { TotalBudget float64 `json:"total_budget"` TotalSpend float64 `json:"total_spend"` RemainingBudget float64 `json:"remaining_budget"` BudgetCount int `json:"budget_count"` OverBudgetCount int `json:"over_budget_count"` } `json:"budget_summary"` // Expense summary ExpenseSummary struct { TotalExpenses float64 `json:"total_expenses"` PendingExpenses float64 `json:"pending_expenses"` ApprovedExpenses float64 `json:"approved_expenses"` ThisMonthExpenses float64 `json:"this_month_expenses"` ExpenseCount int `json:"expense_count"` PendingCount int `json:"pending_count"` } `json:"expense_summary"` // Sponsorship summary SponsorshipSummary struct { TotalSponsors int64 `json:"total_sponsors"` ActiveSponsors int64 `json:"active_sponsors"` TotalValue float64 `json:"total_value"` ReceivedThisYear float64 `json:"received_this_year"` } `json:"sponsorship_summary"` // Recent activities RecentExpenses []models.Expense `json:"recent_expenses"` RecentSponsorships []models.Sponsorship `json:"recent_sponsorships"` } // Get budget summary var budgets []models.Budget if err := fc.db.Where("active = ?", true).Find(&budgets).Error; err == nil { for _, budget := range budgets { dashboard.BudgetSummary.TotalBudget += budget.YearlyLimit currentSpend := fc.calculateBudgetSpend(budget.ID) dashboard.BudgetSummary.TotalSpend += currentSpend if currentSpend > budget.YearlyLimit { dashboard.BudgetSummary.OverBudgetCount++ } } dashboard.BudgetSummary.BudgetCount = len(budgets) dashboard.BudgetSummary.RemainingBudget = dashboard.BudgetSummary.TotalBudget - dashboard.BudgetSummary.TotalSpend } // Get expense summary fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Scan(&dashboard.ExpenseSummary.TotalExpenses) fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "pending").Scan(&dashboard.ExpenseSummary.PendingExpenses) fc.db.Model(&models.Expense{}).Select("COALESCE(SUM(total_amount), 0)").Where("status = ?", "approved").Scan(&dashboard.ExpenseSummary.ApprovedExpenses) var expenseCount, pendingCount int64 fc.db.Model(&models.Expense{}).Count(&expenseCount) fc.db.Model(&models.Expense{}).Where("status = ?", "pending").Count(&pendingCount) dashboard.ExpenseSummary.ExpenseCount = int(expenseCount) dashboard.ExpenseSummary.PendingCount = int(pendingCount) // This month expenses now := time.Now() startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) fc.db.Model(&models.Expense{}).Where("expense_date >= ?", startOfMonth).Select("COALESCE(SUM(total_amount), 0)").Scan(&dashboard.ExpenseSummary.ThisMonthExpenses) // Get sponsorship summary fc.db.Model(&models.Sponsorship{}).Count(&dashboard.SponsorshipSummary.TotalSponsors) fc.db.Model(&models.Sponsorship{}).Where("status = ? AND end_date >= ?", "active", time.Now()).Count(&dashboard.SponsorshipSummary.ActiveSponsors) fc.db.Model(&models.Sponsorship{}).Where("status = ?", "active").Select("COALESCE(SUM(total_value), 0)").Scan(&dashboard.SponsorshipSummary.TotalValue) currentYear := now.Year() fc.db.Model(&models.SponsorshipPayment{}). Where("EXTRACT(YEAR FROM payment_date) = ? AND status = ?", currentYear, "received"). Select("COALESCE(SUM(amount), 0)").Scan(&dashboard.SponsorshipSummary.ReceivedThisYear) // Get recent expenses (last 10) fc.db.Preload("Budget").Order("created_at DESC").Limit(10).Find(&dashboard.RecentExpenses) // Get recent sponsorships (last 5) fc.db.Order("created_at DESC").Limit(5).Find(&dashboard.RecentSponsorships) c.JSON(http.StatusOK, dashboard) } // Financial Reports Management // GetFinancialReports retrieves all financial reports func (fc *FinancialController) GetFinancialReports(c *gin.Context) { var reports []models.FinancialReport if err := fc.db.Order("created_at DESC").Find(&reports).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve reports"}) return } c.JSON(http.StatusOK, reports) } // GenerateFinancialReport generates a new financial report func (fc *FinancialController) GenerateFinancialReport(c *gin.Context) { var request struct { Name string `json:"name" binding:"required"` Type string `json:"type" binding:"required"` Period string `json:"period" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get current user ID from context userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } report := models.FinancialReport{ Name: request.Name, Type: request.Type, Period: request.Period, GeneratedAt: time.Now(), CreatedBy: userID.(uint), } // Generate report data based on type reportData, summary := fc.generateReportData(request.Type, request.Period) report.ReportData = reportData report.Summary = summary if err := fc.db.Create(&report).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create report"}) return } c.JSON(http.StatusCreated, report) } // GetFinancialReport retrieves a single financial report func (fc *FinancialController) GetFinancialReport(c *gin.Context) { id := c.Param("id") var report models.FinancialReport if err := fc.db.First(&report, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Report not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve report"}) return } c.JSON(http.StatusOK, report) } // DeleteFinancialReport deletes a financial report func (fc *FinancialController) DeleteFinancialReport(c *gin.Context) { id := c.Param("id") if err := fc.db.Delete(&models.FinancialReport{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete report"}) return } c.JSON(http.StatusOK, gin.H{"message": "Report deleted successfully"}) } // Financial Settings Management // GetFinancialSettings retrieves financial settings func (fc *FinancialController) GetFinancialSettings(c *gin.Context) { var settings models.FinancialSettings // Get or create settings (singleton pattern) if err := fc.db.First(&settings).Error; err != nil { if err == gorm.ErrRecordNotFound { // Create default settings settings = models.FinancialSettings{ DefaultCurrency: "CZK", DefaultVATRate: 21, FiscalYearStart: "01-01", ExpenseApprovalRequired: true, MaxExpenseAutoApprove: 1000, BudgetAlertEnabled: true, BudgetAlertThreshold: 80, SponsorshipAlertEnabled: true, OCRServiceEnabled: true, OCRProvider: "tesseract", } fc.db.Create(&settings) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve settings"}) return } } c.JSON(http.StatusOK, settings) } // UpdateFinancialSettings updates financial settings func (fc *FinancialController) UpdateFinancialSettings(c *gin.Context) { var settings models.FinancialSettings // Get existing settings if err := fc.db.First(&settings).Error; err != nil { if err == gorm.ErrRecordNotFound { // Create if not exists if err := c.ShouldBindJSON(&settings); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get current user ID from context userID, exists := c.Get("user_id") if exists { settings.UpdatedBy = userID.(uint) } if err := fc.db.Create(&settings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create settings"}) return } } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve settings"}) return } } else { // Update existing var updateData models.FinancialSettings if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields settings.DefaultCurrency = updateData.DefaultCurrency settings.DefaultVATRate = updateData.DefaultVATRate settings.FiscalYearStart = updateData.FiscalYearStart settings.ExpenseApprovalRequired = updateData.ExpenseApprovalRequired settings.MaxExpenseAutoApprove = updateData.MaxExpenseAutoApprove settings.BudgetAlertEnabled = updateData.BudgetAlertEnabled settings.BudgetAlertThreshold = updateData.BudgetAlertThreshold settings.SponsorshipAlertEnabled = updateData.SponsorshipAlertEnabled settings.OCRServiceEnabled = updateData.OCRServiceEnabled settings.OCRProvider = updateData.OCRProvider // Get current user ID from context userID, exists := c.Get("user_id") if exists { settings.UpdatedBy = userID.(uint) } if err := fc.db.Save(&settings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"}) return } } c.JSON(http.StatusOK, settings) } // Helper function to generate report data func (fc *FinancialController) generateReportData(reportType, period string) (string, string) { // This is a placeholder implementation // In a real implementation, you would generate comprehensive JSON data based on the report type and period reportData := `{ "expenses": {"total": 0, "by_category": {}, "by_month": {}}, "budgets": {"total": 0, "utilization": {}}, "sponsorships": {"total": 0, "by_status": {}}, "generated_at": "` + time.Now().Format(time.RFC3339) + `" }` summary := fmt.Sprintf("Financial report for %s - %s", period, reportType) return reportData, summary }