Files
MyClub/internal/controllers/financial_controller.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

1167 lines
37 KiB
Go

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
}