feat(transaction-recurring): #100 add summary for recurring transactions #131

Merged
tim merged 1 commits from 100-add-summary into prod 2025-05-26 16:31:51 +00:00
3 changed files with 70 additions and 8 deletions

View File

@@ -10,6 +10,7 @@ import (
"spend-sparrow/utils" "spend-sparrow/utils"
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/google/uuid"
) )
type TreasureChest interface { type TreasureChest interface {
@@ -51,7 +52,15 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
return return
} }
comp := t.TreasureChest(treasureChests) transactionsRecurring, err := h.transactionRecurring.GetAll(user)
if err != nil {
handleError(w, r, err)
return
}
monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
comp := t.TreasureChest(treasureChests, monthlySums)
h.r.RenderLayout(r, w, comp, user) h.r.RenderLayout(r, w, comp, user)
} }
} }
@@ -94,7 +103,8 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
if r.URL.Query().Get("edit") == "true" { if r.URL.Query().Get("edit") == "true" {
comp = t.EditTreasureChest(treasureChest, treasureChests, transactionsRec) comp = t.EditTreasureChest(treasureChest, treasureChests, transactionsRec)
} else { } else {
comp = t.TreasureChestItem(treasureChest) monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
comp = t.TreasureChestItem(treasureChest, monthlySums)
} }
h.r.Render(r, w, comp) h.r.Render(r, w, comp)
} }
@@ -129,7 +139,16 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
} }
} }
comp := t.TreasureChestItem(treasureChest) transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String())
if err != nil {
handleError(w, r, err)
return
}
treasureChests := make([]*types.TreasureChest, 1)
treasureChests[0] = treasureChest
monthlySums := h.calculateMonthlySums(treasureChests, transactionsRecurring)
comp := t.TreasureChestItem(treasureChest, monthlySums)
h.r.Render(r, w, comp) h.r.Render(r, w, comp)
} }
} }
@@ -151,3 +170,19 @@ func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
} }
} }
} }
func (h TreasureChestImpl) calculateMonthlySums(
treasureChests []*types.TreasureChest,
transactionsRecurring []*types.TransactionRecurring,
) map[uuid.UUID]int64 {
monthlySums := make(map[uuid.UUID]int64)
for _, tc := range treasureChests {
monthlySums[tc.Id] = 0
}
for _, t := range transactionsRecurring {
if t.TreasureChestId != nil && t.Value > 0 && t.IntervalMonths > 0 {
monthlySums[*t.TreasureChestId] += t.Value / t.IntervalMonths
}
}
return monthlySums
}

View File

@@ -29,6 +29,7 @@ var (
type TransactionRecurring interface { type TransactionRecurring interface {
Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
GetAll(user *types.User) ([]*types.TransactionRecurring, error)
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error)
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
Delete(user *types.User, id string) error Delete(user *types.User, id string) error
@@ -158,6 +159,27 @@ func (s TransactionRecurringImpl) Update(
return transactionRecurring, nil return transactionRecurring, nil
} }
func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) {
transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc()
if user == nil {
return nil, ErrUnauthorized
}
transactionRecurrings := make([]*types.TransactionRecurring, 0)
err := s.db.Select(&transactionRecurrings, `
SELECT *
FROM transaction_recurring
WHERE user_id = ?
ORDER BY created_at DESC`,
user.Id)
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
if err != nil {
return nil, err
}
return transactionRecurrings, nil
}
func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc() transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc()
if user == nil { if user == nil {

View File

@@ -5,7 +5,7 @@ import "spend-sparrow/template/svg"
import "spend-sparrow/types" import "spend-sparrow/types"
import "github.com/google/uuid" import "github.com/google/uuid"
templ TreasureChest(treasureChests []*types.TreasureChest) { templ TreasureChest(treasureChests []*types.TreasureChest, monthlySums map[uuid.UUID]int64) {
<div class="max-w-6xl mt-10 mx-auto"> <div class="max-w-6xl mt-10 mx-auto">
<button <button
hx-get="/treasurechest/new" hx-get="/treasurechest/new"
@@ -18,7 +18,7 @@ templ TreasureChest(treasureChests []*types.TreasureChest) {
</button> </button>
<div id="treasurechest-items" class="my-6 flex flex-col"> <div id="treasurechest-items" class="my-6 flex flex-col">
for _, treasureChest := range treasureChests { for _, treasureChest := range treasureChests {
@TreasureChestItem(treasureChest) @TreasureChestItem(treasureChest, monthlySums)
} }
</div> </div>
</div> </div>
@@ -114,7 +114,7 @@ templ EditTreasureChest(treasureChest *types.TreasureChest, parents []*types.Tre
</div> </div>
} }
templ TreasureChestItem(treasureChest *types.TreasureChest) { templ TreasureChestItem(treasureChest *types.TreasureChest, monthlySums map[uuid.UUID]int64) {
{{ {{
var indentation string var indentation string
viewTransactions := "" viewTransactions := ""
@@ -128,11 +128,16 @@ templ TreasureChestItem(treasureChest *types.TreasureChest) {
<div id={ "treasurechest-" + treasureChest.Id.String() } class={ "border-1 border-gray-300 p-4 bg-gray-50 rounded-lg" + indentation }> <div id={ "treasurechest-" + treasureChest.Id.String() } class={ "border-1 border-gray-300 p-4 bg-gray-50 rounded-lg" + indentation }>
<div class="text-xl flex justify-end items-center gap-4"> <div class="text-xl flex justify-end items-center gap-4">
<p class="mr-auto">{ treasureChest.Name }</p> <p class="mr-auto">{ treasureChest.Name }</p>
<p class="mr-20 text-gray-600">
if treasureChest.ParentId != nil {
+ { displayBalance(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm">&nbsp;per month</span>
}
</p>
if treasureChest.ParentId != nil { if treasureChest.ParentId != nil {
if treasureChest.CurrentBalance < 0 { if treasureChest.CurrentBalance < 0 {
<p class="mr-20 text-red-700">{ displayBalance(treasureChest.CurrentBalance) }</p> <p class="mr-20 min-w-20 text-right text-red-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
} else { } else {
<p class="mr-20 text-green-700">{ displayBalance(treasureChest.CurrentBalance) }</p> <p class="mr-20 min-w-20 text-right text-green-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
} }
} }
<a <a