606 lines
18 KiB
Go
606 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"accounting-app/internal/models"
|
|
"accounting-app/internal/repository"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
// ExcelExportService handles Excel export functionality
|
|
type ExcelExportService struct {
|
|
reportRepo *repository.ReportRepository
|
|
transactionRepo *repository.TransactionRepository
|
|
exchangeRateRepo *repository.ExchangeRateRepository
|
|
}
|
|
|
|
// NewExcelExportService creates a new ExcelExportService instance
|
|
func NewExcelExportService(reportRepo *repository.ReportRepository, transactionRepo *repository.TransactionRepository, exchangeRateRepo *repository.ExchangeRateRepository) *ExcelExportService {
|
|
return &ExcelExportService{
|
|
reportRepo: reportRepo,
|
|
transactionRepo: transactionRepo,
|
|
exchangeRateRepo: exchangeRateRepo,
|
|
}
|
|
}
|
|
|
|
// ExportReportToExcel generates an Excel report with transaction details and summary statistics
|
|
func (s *ExcelExportService) ExportReportToExcel(userID uint, req ExportReportRequest) ([]byte, error) {
|
|
// Create new Excel file
|
|
f := excelize.NewFile()
|
|
defer f.Close()
|
|
|
|
// Create sheets
|
|
summarySheet := "Summary"
|
|
transactionsSheet := "Transactions"
|
|
categoriesSheet := "Categories"
|
|
|
|
// Rename default sheet to Summary
|
|
f.SetSheetName("Sheet1", summarySheet)
|
|
|
|
// Create other sheets
|
|
_, err := f.NewSheet(transactionsSheet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create transactions sheet: %w", err)
|
|
}
|
|
|
|
_, err = f.NewSheet(categoriesSheet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create categories sheet: %w", err)
|
|
}
|
|
|
|
// Get data
|
|
summary, err := s.getTransactionSummary(userID, req.StartDate, req.EndDate, req.TargetCurrency)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transaction summary: %w", err)
|
|
}
|
|
|
|
categoryExpenseSummary, err := s.getCategorySummary(userID, req.StartDate, req.EndDate, models.TransactionTypeExpense, req.TargetCurrency)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get category expense summary: %w", err)
|
|
}
|
|
|
|
categoryIncomeSummary, err := s.getCategorySummary(userID, req.StartDate, req.EndDate, models.TransactionTypeIncome, req.TargetCurrency)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get category income summary: %w", err)
|
|
}
|
|
|
|
transactions, err := s.transactionRepo.GetByDateRange(userID, req.StartDate, req.EndDate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
|
}
|
|
|
|
// Populate sheets
|
|
if err := s.populateSummarySheet(f, summarySheet, summary, req); err != nil {
|
|
return nil, fmt.Errorf("failed to populate summary sheet: %w", err)
|
|
}
|
|
|
|
if err := s.populateTransactionsSheet(f, transactionsSheet, transactions); err != nil {
|
|
return nil, fmt.Errorf("failed to populate transactions sheet: %w", err)
|
|
}
|
|
|
|
if err := s.populateCategoriesSheet(f, categoriesSheet, categoryExpenseSummary, categoryIncomeSummary); err != nil {
|
|
return nil, fmt.Errorf("failed to populate categories sheet: %w", err)
|
|
}
|
|
|
|
// Set active sheet to Summary
|
|
f.SetActiveSheet(0)
|
|
|
|
// Save to buffer
|
|
buf, err := f.WriteToBuffer()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to write Excel file: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// populateSummarySheet populates the summary sheet with report metadata and summary statistics
|
|
func (s *ExcelExportService) populateSummarySheet(f *excelize.File, sheetName string, summary *summaryData, req ExportReportRequest) error {
|
|
// Define styles
|
|
titleStyle, err := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
Size: 16,
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "center",
|
|
Vertical: "center",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
headerStyle, err := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
Size: 12,
|
|
},
|
|
Fill: excelize.Fill{
|
|
Type: "pattern",
|
|
Color: []string{"#D3D3D3"},
|
|
Pattern: 1,
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "left",
|
|
Vertical: "center",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
valueStyle, err := f.NewStyle(&excelize.Style{
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "right",
|
|
Vertical: "center",
|
|
},
|
|
NumFmt: 2, // 0.00 format
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set column widths
|
|
f.SetColWidth(sheetName, "A", "A", 25)
|
|
f.SetColWidth(sheetName, "B", "B", 20)
|
|
|
|
// Title
|
|
f.SetCellValue(sheetName, "A1", "Financial Report")
|
|
f.SetCellStyle(sheetName, "A1", "B1", titleStyle)
|
|
f.MergeCell(sheetName, "A1", "B1")
|
|
|
|
// Report period
|
|
row := 3
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Report Period:")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("%s to %s", req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02")))
|
|
row++
|
|
|
|
// Generated date
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Generated:")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), time.Now().Format("2006-01-02 15:04:05"))
|
|
row++
|
|
|
|
// Currency
|
|
currencyStr := "Mixed"
|
|
if req.TargetCurrency != nil {
|
|
currencyStr = string(*req.TargetCurrency)
|
|
}
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Currency:")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), currencyStr)
|
|
row += 2
|
|
|
|
// Summary statistics header
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Summary Statistics")
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("B%d", row), headerStyle)
|
|
f.MergeCell(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("B%d", row))
|
|
row++
|
|
|
|
// Total Income
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Total Income")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), summary.TotalIncome)
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), valueStyle)
|
|
row++
|
|
|
|
// Total Expense
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Total Expense")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), summary.TotalExpense)
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), valueStyle)
|
|
row++
|
|
|
|
// Balance
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Balance")
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), summary.Balance)
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), valueStyle)
|
|
|
|
// Apply bold style to balance
|
|
balanceStyle, err := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "right",
|
|
Vertical: "center",
|
|
},
|
|
NumFmt: 2,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("B%d", row), balanceStyle)
|
|
|
|
return nil
|
|
}
|
|
|
|
// populateTransactionsSheet populates the transactions sheet with transaction details
|
|
func (s *ExcelExportService) populateTransactionsSheet(f *excelize.File, sheetName string, transactions []models.Transaction) error {
|
|
// Define styles
|
|
headerStyle, err := f.NewStyle(&excelize.Style{
|
|
Fill: excelize.Fill{
|
|
Type: "pattern",
|
|
Color: []string{"#4472C4"},
|
|
Pattern: 1,
|
|
},
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
Color: "#FFFFFF",
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "center",
|
|
Vertical: "center",
|
|
},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#000000", Style: 1},
|
|
{Type: "top", Color: "#000000", Style: 1},
|
|
{Type: "bottom", Color: "#000000", Style: 1},
|
|
{Type: "right", Color: "#000000", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set column widths
|
|
f.SetColWidth(sheetName, "A", "A", 12) // Date
|
|
f.SetColWidth(sheetName, "B", "B", 10) // Type
|
|
f.SetColWidth(sheetName, "C", "C", 20) // Category
|
|
f.SetColWidth(sheetName, "D", "D", 20) // Account
|
|
f.SetColWidth(sheetName, "E", "E", 12) // Amount
|
|
f.SetColWidth(sheetName, "F", "F", 10) // Currency
|
|
f.SetColWidth(sheetName, "G", "G", 40) // Note
|
|
|
|
// Headers
|
|
headers := []string{"Date", "Type", "Category", "Account", "Amount", "Currency", "Note"}
|
|
for i, header := range headers {
|
|
cell := fmt.Sprintf("%s1", string(rune('A'+i)))
|
|
f.SetCellValue(sheetName, cell, header)
|
|
f.SetCellStyle(sheetName, cell, cell, headerStyle)
|
|
}
|
|
|
|
// Data rows
|
|
for i, txn := range transactions {
|
|
row := i + 2
|
|
|
|
// Date
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), txn.TransactionDate.Format("2006-01-02"))
|
|
|
|
// Type
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), string(txn.Type))
|
|
|
|
// Category
|
|
categoryName := ""
|
|
if txn.Category.Name != "" {
|
|
categoryName = txn.Category.Name
|
|
}
|
|
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), categoryName)
|
|
|
|
// Account
|
|
accountName := ""
|
|
if txn.Account.Name != "" {
|
|
accountName = txn.Account.Name
|
|
}
|
|
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), accountName)
|
|
|
|
// Amount
|
|
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), txn.Amount)
|
|
|
|
// Currency
|
|
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), string(txn.Currency))
|
|
|
|
// Note
|
|
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), txn.Note)
|
|
}
|
|
|
|
// Apply table style
|
|
if len(transactions) > 0 {
|
|
lastRow := len(transactions) + 1
|
|
// Add borders to all cells
|
|
for row := 2; row <= lastRow; row++ {
|
|
for col := 'A'; col <= 'G'; col++ {
|
|
cell := fmt.Sprintf("%c%d", col, row)
|
|
style, _ := f.NewStyle(&excelize.Style{
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, cell, cell, style)
|
|
}
|
|
}
|
|
|
|
// Format amount column
|
|
amountStyle, _ := f.NewStyle(&excelize.Style{
|
|
NumFmt: 2, // 0.00 format
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, "E2", fmt.Sprintf("E%d", lastRow), amountStyle)
|
|
}
|
|
|
|
// Freeze header row
|
|
f.SetPanes(sheetName, &excelize.Panes{
|
|
Freeze: true,
|
|
XSplit: 0,
|
|
YSplit: 1,
|
|
TopLeftCell: "A2",
|
|
ActivePane: "bottomLeft",
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// populateCategoriesSheet populates the categories sheet with category breakdown
|
|
func (s *ExcelExportService) populateCategoriesSheet(f *excelize.File, sheetName string, expenseCategories, incomeCategories []categoryData) error {
|
|
// Define styles
|
|
titleStyle, err := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
Size: 14,
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "left",
|
|
Vertical: "center",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
headerStyle, err := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{
|
|
Bold: true,
|
|
Color: "#FFFFFF",
|
|
},
|
|
Fill: excelize.Fill{
|
|
Type: "pattern",
|
|
Color: []string{"#4472C4"},
|
|
Pattern: 1,
|
|
},
|
|
Alignment: &excelize.Alignment{
|
|
Horizontal: "center",
|
|
Vertical: "center",
|
|
},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#000000", Style: 1},
|
|
{Type: "top", Color: "#000000", Style: 1},
|
|
{Type: "bottom", Color: "#000000", Style: 1},
|
|
{Type: "right", Color: "#000000", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set column widths
|
|
f.SetColWidth(sheetName, "A", "A", 25)
|
|
f.SetColWidth(sheetName, "B", "B", 15)
|
|
f.SetColWidth(sheetName, "C", "C", 12)
|
|
f.SetColWidth(sheetName, "D", "D", 15)
|
|
|
|
row := 1
|
|
|
|
// Expense Categories Section
|
|
if len(expenseCategories) > 0 {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Expense by Category")
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("A%d", row), titleStyle)
|
|
row++
|
|
|
|
// Headers
|
|
headers := []string{"Category", "Amount", "Count", "Percentage"}
|
|
for i, header := range headers {
|
|
cell := fmt.Sprintf("%s%d", string(rune('A'+i)), row)
|
|
f.SetCellValue(sheetName, cell, header)
|
|
f.SetCellStyle(sheetName, cell, cell, headerStyle)
|
|
}
|
|
row++
|
|
|
|
// Data
|
|
for _, cat := range expenseCategories {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), cat.CategoryName)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), cat.TotalAmount)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), cat.Count)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), fmt.Sprintf("%.1f%%", cat.Percentage))
|
|
|
|
// Apply borders
|
|
for col := 'A'; col <= 'D'; col++ {
|
|
cell := fmt.Sprintf("%c%d", col, row)
|
|
style, _ := f.NewStyle(&excelize.Style{
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, cell, cell, style)
|
|
}
|
|
|
|
// Format amount column
|
|
amountStyle, _ := f.NewStyle(&excelize.Style{
|
|
NumFmt: 2,
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), amountStyle)
|
|
|
|
row++
|
|
}
|
|
row += 2
|
|
}
|
|
|
|
// Income Categories Section
|
|
if len(incomeCategories) > 0 {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Income by Category")
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("A%d", row), titleStyle)
|
|
row++
|
|
|
|
// Headers
|
|
headers := []string{"Category", "Amount", "Count", "Percentage"}
|
|
for i, header := range headers {
|
|
cell := fmt.Sprintf("%s%d", string(rune('A'+i)), row)
|
|
f.SetCellValue(sheetName, cell, header)
|
|
f.SetCellStyle(sheetName, cell, cell, headerStyle)
|
|
}
|
|
row++
|
|
|
|
// Data
|
|
for _, cat := range incomeCategories {
|
|
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), cat.CategoryName)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), cat.TotalAmount)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), cat.Count)
|
|
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), fmt.Sprintf("%.1f%%", cat.Percentage))
|
|
|
|
// Apply borders
|
|
for col := 'A'; col <= 'D'; col++ {
|
|
cell := fmt.Sprintf("%c%d", col, row)
|
|
style, _ := f.NewStyle(&excelize.Style{
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, cell, cell, style)
|
|
}
|
|
|
|
// Format amount column
|
|
amountStyle, _ := f.NewStyle(&excelize.Style{
|
|
NumFmt: 2,
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#D3D3D3", Style: 1},
|
|
{Type: "top", Color: "#D3D3D3", Style: 1},
|
|
{Type: "bottom", Color: "#D3D3D3", Style: 1},
|
|
{Type: "right", Color: "#D3D3D3", Style: 1},
|
|
},
|
|
})
|
|
f.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), amountStyle)
|
|
|
|
row++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTransactionSummary retrieves transaction summary for the report (reusing from PDF service)
|
|
func (s *ExcelExportService) getTransactionSummary(userID uint, startDate, endDate time.Time, targetCurrency *models.Currency) (*summaryData, error) {
|
|
summaries, err := s.reportRepo.GetTransactionSummaryByCurrency(userID, startDate, endDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := &summaryData{}
|
|
|
|
// If target currency is specified, convert all to that currency
|
|
if targetCurrency != nil {
|
|
for _, summary := range summaries {
|
|
if summary.Currency == *targetCurrency {
|
|
result.TotalIncome += summary.TotalIncome
|
|
result.TotalExpense += summary.TotalExpense
|
|
} else {
|
|
// Get exchange rate
|
|
rate, err := s.exchangeRateRepo.GetByCurrencyPairAndDate(summary.Currency, *targetCurrency, time.Now())
|
|
if err != nil {
|
|
// Try inverse rate
|
|
inverseRate, inverseErr := s.exchangeRateRepo.GetByCurrencyPairAndDate(*targetCurrency, summary.Currency, time.Now())
|
|
if inverseErr != nil {
|
|
// If no rate found, skip this currency
|
|
continue
|
|
}
|
|
rate = &models.ExchangeRate{
|
|
FromCurrency: summary.Currency,
|
|
ToCurrency: *targetCurrency,
|
|
Rate: 1.0 / inverseRate.Rate,
|
|
}
|
|
}
|
|
result.TotalIncome += summary.TotalIncome * rate.Rate
|
|
result.TotalExpense += summary.TotalExpense * rate.Rate
|
|
}
|
|
}
|
|
} else {
|
|
// No target currency, just sum all (assuming same currency or user doesn't care)
|
|
for _, summary := range summaries {
|
|
result.TotalIncome += summary.TotalIncome
|
|
result.TotalExpense += summary.TotalExpense
|
|
}
|
|
}
|
|
|
|
result.Balance = result.TotalIncome - result.TotalExpense
|
|
return result, nil
|
|
}
|
|
|
|
// getCategorySummary retrieves category summary for the report (reusing from PDF service)
|
|
func (s *ExcelExportService) getCategorySummary(userID uint, startDate, endDate time.Time, txnType models.TransactionType, targetCurrency *models.Currency) ([]categoryData, error) {
|
|
summaries, err := s.reportRepo.GetCategorySummaryByCurrency(userID, startDate, endDate, txnType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group by category and convert currency if needed
|
|
categoryMap := make(map[uint]*categoryData)
|
|
|
|
for _, summary := range summaries {
|
|
if categoryMap[summary.CategoryID] == nil {
|
|
categoryMap[summary.CategoryID] = &categoryData{
|
|
CategoryName: summary.CategoryName,
|
|
TotalAmount: 0,
|
|
Count: 0,
|
|
}
|
|
}
|
|
|
|
amount := summary.TotalAmount
|
|
if targetCurrency != nil && summary.Currency != *targetCurrency {
|
|
// Get exchange rate
|
|
rate, err := s.exchangeRateRepo.GetByCurrencyPairAndDate(summary.Currency, *targetCurrency, time.Now())
|
|
if err != nil {
|
|
// Try inverse rate
|
|
inverseRate, inverseErr := s.exchangeRateRepo.GetByCurrencyPairAndDate(*targetCurrency, summary.Currency, time.Now())
|
|
if inverseErr != nil {
|
|
// If no rate found, skip this entry
|
|
continue
|
|
}
|
|
rate = &models.ExchangeRate{
|
|
FromCurrency: summary.Currency,
|
|
ToCurrency: *targetCurrency,
|
|
Rate: 1.0 / inverseRate.Rate,
|
|
}
|
|
}
|
|
amount = summary.TotalAmount * rate.Rate
|
|
}
|
|
|
|
categoryMap[summary.CategoryID].TotalAmount += amount
|
|
categoryMap[summary.CategoryID].Count += summary.Count
|
|
}
|
|
|
|
// Convert map to slice and calculate percentages
|
|
result := make([]categoryData, 0, len(categoryMap))
|
|
var total float64
|
|
for _, cat := range categoryMap {
|
|
total += cat.TotalAmount
|
|
result = append(result, *cat)
|
|
}
|
|
|
|
// Calculate percentages
|
|
for i := range result {
|
|
if total > 0 {
|
|
result[i].Percentage = (result[i].TotalAmount / total) * 100
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|