From 047e392ac3bdaaead71cd3e1fe9b6d07405a91ca Mon Sep 17 00:00:00 2001 From: juancwu Date: Tue, 17 Mar 2026 17:04:21 +0000 Subject: [PATCH] feat: show report on payment method --- internal/model/report.go | 8 ++++++ internal/repository/expense.go | 21 ++++++++++++++ internal/service/report.go | 6 ++++ internal/ui/pages/app_space_reports.templ | 35 +++++++++++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/internal/model/report.go b/internal/model/report.go index 46945ae..8586c99 100644 --- a/internal/model/report.go +++ b/internal/model/report.go @@ -16,8 +16,16 @@ type MonthlySpending struct { Total decimal.Decimal `db:"total"` } +type PaymentMethodExpenseSummary struct { + PaymentMethodID string `db:"payment_method_id"` + PaymentMethodName string `db:"payment_method_name"` + PaymentMethodType string `db:"payment_method_type"` + TotalAmount decimal.Decimal `db:"total_amount"` +} + type SpendingReport struct { ByTag []*TagExpenseSummary + ByPaymentMethod []*PaymentMethodExpenseSummary DailySpending []*DailySpending MonthlySpending []*MonthlySpending TopExpenses []*ExpenseWithTagsAndMethod diff --git a/internal/repository/expense.go b/internal/repository/expense.go index 9235630..6d75548 100644 --- a/internal/repository/expense.go +++ b/internal/repository/expense.go @@ -30,6 +30,7 @@ type ExpenseRepository interface { GetMonthlySpending(spaceID string, from, to time.Time) ([]*model.MonthlySpending, error) GetTopExpenses(spaceID string, from, to time.Time, limit int) ([]*model.Expense, error) GetIncomeVsExpenseSummary(spaceID string, from, to time.Time) (decimal.Decimal, decimal.Decimal, error) + GetExpensesByPaymentMethod(spaceID string, from, to time.Time) ([]*model.PaymentMethodExpenseSummary, error) } type expenseRepository struct { @@ -324,3 +325,23 @@ func (r *expenseRepository) GetIncomeVsExpenseSummary(spaceID string, from, to t } return income, expenseTotal, nil } + +func (r *expenseRepository) GetExpensesByPaymentMethod(spaceID string, from, to time.Time) ([]*model.PaymentMethodExpenseSummary, error) { + var summaries []*model.PaymentMethodExpenseSummary + query := ` + SELECT COALESCE(pm.id, 'cash') as payment_method_id, + COALESCE(pm.name, 'Cash') as payment_method_name, + COALESCE(pm.type, 'cash') as payment_method_type, + SUM(CAST(e.amount AS DECIMAL)) as total_amount + FROM expenses e + LEFT JOIN payment_methods pm ON e.payment_method_id = pm.id + WHERE e.space_id = $1 AND e.type = 'expense' AND e.date >= $2 AND e.date <= $3 + GROUP BY pm.id, pm.name, pm.type + ORDER BY total_amount DESC; + ` + err := r.db.Select(&summaries, query, spaceID, from, to) + if err != nil { + return nil, err + } + return summaries, nil +} diff --git a/internal/service/report.go b/internal/service/report.go index 4de051b..fb6a55c 100644 --- a/internal/service/report.go +++ b/internal/service/report.go @@ -61,6 +61,11 @@ func (s *ReportService) GetSpendingReport(spaceID string, from, to time.Time) (* } } + byPaymentMethod, err := s.expenseRepo.GetExpensesByPaymentMethod(spaceID, from, to) + if err != nil { + return nil, err + } + totalIncome, totalExpenses, err := s.expenseRepo.GetIncomeVsExpenseSummary(spaceID, from, to) if err != nil { return nil, err @@ -68,6 +73,7 @@ func (s *ReportService) GetSpendingReport(spaceID string, from, to time.Time) (* return &model.SpendingReport{ ByTag: byTag, + ByPaymentMethod: byPaymentMethod, DailySpending: daily, MonthlySpending: monthly, TopExpenses: topWithTags, diff --git a/internal/ui/pages/app_space_reports.templ b/internal/ui/pages/app_space_reports.templ index 82879c9..35e3852 100644 --- a/internal/ui/pages/app_space_reports.templ +++ b/internal/ui/pages/app_space_reports.templ @@ -123,6 +123,41 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T

No tagged expenses in this period.

} + // Spending by Payment Method (Doughnut chart) + if len(report.ByPaymentMethod) > 0 { +
+

Spending by Payment Method

+ {{ + pmLabels := make([]string, len(report.ByPaymentMethod)) + pmData := make([]float64, len(report.ByPaymentMethod)) + pmColors := make([]string, len(report.ByPaymentMethod)) + for i, pm := range report.ByPaymentMethod { + pmLabels[i] = pm.PaymentMethodName + pmData[i] = pm.TotalAmount.InexactFloat64() + pmColors[i] = defaultChartColors[i%len(defaultChartColors)] + } + }} + @chart.Chart(chart.Props{ + Variant: chart.VariantDoughnut, + ShowLegend: true, + Data: chart.Data{ + Labels: pmLabels, + Datasets: []chart.Dataset{ + { + Label: "Spending", + Data: pmData, + BackgroundColor: pmColors, + }, + }, + }, + }) +
+ } else { +
+

Spending by Payment Method

+

No payment method data in this period.

+
+ } // Spending Over Time (Bar chart) if len(report.DailySpending) > 0 || len(report.MonthlySpending) > 0 {