init
This commit is contained in:
605
internal/service/excel_export_service.go
Normal file
605
internal/service/excel_export_service.go
Normal file
@@ -0,0 +1,605 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user