feat: show report on payment method
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m37s

This commit is contained in:
juancwu 2026-03-17 17:04:21 +00:00
commit 047e392ac3
4 changed files with 70 additions and 0 deletions

View file

@ -16,8 +16,16 @@ type MonthlySpending struct {
Total decimal.Decimal `db:"total"` 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 { type SpendingReport struct {
ByTag []*TagExpenseSummary ByTag []*TagExpenseSummary
ByPaymentMethod []*PaymentMethodExpenseSummary
DailySpending []*DailySpending DailySpending []*DailySpending
MonthlySpending []*MonthlySpending MonthlySpending []*MonthlySpending
TopExpenses []*ExpenseWithTagsAndMethod TopExpenses []*ExpenseWithTagsAndMethod

View file

@ -30,6 +30,7 @@ type ExpenseRepository interface {
GetMonthlySpending(spaceID string, from, to time.Time) ([]*model.MonthlySpending, error) GetMonthlySpending(spaceID string, from, to time.Time) ([]*model.MonthlySpending, error)
GetTopExpenses(spaceID string, from, to time.Time, limit int) ([]*model.Expense, 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) 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 { type expenseRepository struct {
@ -324,3 +325,23 @@ func (r *expenseRepository) GetIncomeVsExpenseSummary(spaceID string, from, to t
} }
return income, expenseTotal, nil 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
}

View file

@ -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) totalIncome, totalExpenses, err := s.expenseRepo.GetIncomeVsExpenseSummary(spaceID, from, to)
if err != nil { if err != nil {
return nil, err return nil, err
@ -68,6 +73,7 @@ func (s *ReportService) GetSpendingReport(spaceID string, from, to time.Time) (*
return &model.SpendingReport{ return &model.SpendingReport{
ByTag: byTag, ByTag: byTag,
ByPaymentMethod: byPaymentMethod,
DailySpending: daily, DailySpending: daily,
MonthlySpending: monthly, MonthlySpending: monthly,
TopExpenses: topWithTags, TopExpenses: topWithTags,

View file

@ -123,6 +123,41 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T
<p class="text-sm text-muted-foreground">No tagged expenses in this period.</p> <p class="text-sm text-muted-foreground">No tagged expenses in this period.</p>
</div> </div>
} }
// Spending by Payment Method (Doughnut chart)
if len(report.ByPaymentMethod) > 0 {
<div class="border rounded-lg p-4 bg-card text-card-foreground min-w-0 overflow-hidden">
<h3 class="font-semibold mb-2">Spending by Payment Method</h3>
{{
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,
},
},
},
})
</div>
} else {
<div class="border rounded-lg p-4 bg-card text-card-foreground min-w-0">
<h3 class="font-semibold mb-2">Spending by Payment Method</h3>
<p class="text-sm text-muted-foreground">No payment method data in this period.</p>
</div>
}
// Spending Over Time (Bar chart) // Spending Over Time (Bar chart)
if len(report.DailySpending) > 0 || len(report.MonthlySpending) > 0 { if len(report.DailySpending) > 0 || len(report.MonthlySpending) > 0 {
<div class="border rounded-lg p-4 bg-card text-card-foreground min-w-0 overflow-hidden"> <div class="border rounded-lg p-4 bg-card text-card-foreground min-w-0 overflow-hidden">