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 { +No payment method data in this period.
+