Merge branch 'fix/calculation-accuracy' into main
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m37s

Combines the decimal migration (int cents → decimal.Decimal via
shopspring/decimal) with main's handler refactor (split space.go into
domain handlers, WithTx/Paginate helpers, recurring deposit removal).

- Repository layer: WithTx pattern + decimal column names/types
- Handler layer: decimal arithmetic (.Sub/.Add) instead of int operators
- Models: deprecated amount_cents fields kept for SELECT * compatibility
- INSERT statements: old columns set to literal 0 for NOT NULL constraints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
juancwu 2026-03-14 16:48:40 -04:00
commit 89c5d76e5e
No known key found for this signature in database
46 changed files with 661 additions and 539 deletions

View file

@ -14,6 +14,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/paymentmethod"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/radio"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagcombobox"
"github.com/shopspring/decimal"
)
type AddExpenseFormProps struct {
@ -215,7 +216,7 @@ templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
Name: "amount",
ID: "edit-amount-" + exp.ID,
Type: "number",
Value: fmt.Sprintf("%.2f", float64(exp.AmountCents)/100.0),
Value: model.FormatDecimal(exp.Amount),
Attributes: templ.Attributes{"step": "0.01", "required": "true"},
})
</div>
@ -345,7 +346,7 @@ templ ItemSelectorSection(listsWithItems []model.ListWithUncheckedItems, oob boo
</div>
}
templ BalanceCard(spaceID string, balance int, allocated int, oob bool) {
templ BalanceCard(spaceID string, balance decimal.Decimal, allocated decimal.Decimal, oob bool) {
<div
id="balance-card"
class="border rounded-lg p-4 bg-card text-card-foreground"
@ -354,11 +355,11 @@ templ BalanceCard(spaceID string, balance int, allocated int, oob bool) {
}
>
<h2 class="text-lg font-semibold">Current Balance</h2>
<p class={ "text-3xl font-bold", templ.KV("text-destructive", balance < 0) }>
{ fmt.Sprintf("$%.2f", float64(balance)/100.0) }
if allocated > 0 {
<p class={ "text-3xl font-bold", templ.KV("text-destructive", balance.LessThan(decimal.Zero)) }>
{ model.FormatMoney(balance) }
if allocated.GreaterThan(decimal.Zero) {
<span class="text-base font-normal text-muted-foreground">
({ fmt.Sprintf("$%.2f", float64(allocated)/100.0) } in accounts)
({ model.FormatMoney(allocated) } in accounts)
</span>
}
</p>

View file

@ -11,9 +11,10 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/label"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/pagination"
"github.com/shopspring/decimal"
)
templ BalanceSummaryCard(spaceID string, totalBalance int, availableBalance int, oob bool) {
templ BalanceSummaryCard(spaceID string, totalBalance decimal.Decimal, availableBalance decimal.Decimal, oob bool) {
<div
id="accounts-balance-summary"
class="border rounded-lg p-4 bg-card text-card-foreground"
@ -25,20 +26,20 @@ templ BalanceSummaryCard(spaceID string, totalBalance int, availableBalance int,
<div class="grid grid-cols-3 gap-4">
<div>
<p class="text-sm text-muted-foreground">Total Balance</p>
<p class={ "text-xl font-bold", templ.KV("text-destructive", totalBalance < 0) }>
{ fmt.Sprintf("$%.2f", float64(totalBalance)/100.0) }
<p class={ "text-xl font-bold", templ.KV("text-destructive", totalBalance.LessThan(decimal.Zero)) }>
{ model.FormatMoney(totalBalance) }
</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Allocated</p>
<p class="text-xl font-bold">
{ fmt.Sprintf("$%.2f", float64(totalBalance-availableBalance)/100.0) }
{ model.FormatMoney(totalBalance.Sub(availableBalance)) }
</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Available</p>
<p class={ "text-xl font-bold", templ.KV("text-destructive", availableBalance < 0) }>
{ fmt.Sprintf("$%.2f", float64(availableBalance)/100.0) }
<p class={ "text-xl font-bold", templ.KV("text-destructive", availableBalance.LessThan(decimal.Zero)) }>
{ model.FormatMoney(availableBalance) }
</p>
</div>
</div>
@ -60,8 +61,8 @@ templ AccountCard(spaceID string, acct *model.MoneyAccountWithBalance, oob ...bo
<div class="flex justify-between items-start mb-3">
<div>
<h3 class="font-semibold text-lg">{ acct.Name }</h3>
<p class={ "text-2xl font-bold", templ.KV("text-destructive", acct.BalanceCents < 0) }>
{ fmt.Sprintf("$%.2f", float64(acct.BalanceCents)/100.0) }
<p class={ "text-2xl font-bold", templ.KV("text-destructive", acct.Balance.LessThan(decimal.Zero)) }>
{ model.FormatMoney(acct.Balance) }
</p>
</div>
<div class="flex gap-1">
@ -359,11 +360,11 @@ templ TransferHistoryItem(spaceID string, t *model.AccountTransferWithAccount) {
<div class="flex items-center gap-2">
if t.Direction == model.TransferDirectionDeposit {
<span class="font-bold text-green-600 whitespace-nowrap">
+{ fmt.Sprintf("$%.2f", float64(t.AmountCents)/100.0) }
+{ model.FormatMoney(t.Amount) }
</span>
} else {
<span class="font-bold text-destructive whitespace-nowrap">
-{ fmt.Sprintf("$%.2f", float64(t.AmountCents)/100.0) }
-{ model.FormatMoney(t.Amount) }
</span>
}
@button.Button(button.Props{
@ -382,4 +383,3 @@ templ TransferHistoryItem(spaceID string, t *model.AccountTransferWithAccount) {
</div>
</div>
}

View file

@ -73,11 +73,11 @@ templ RecurringItem(spaceID string, re *model.RecurringExpenseWithTagsAndMethod,
<div class="flex items-center gap-1 shrink-0">
if re.Type == model.ExpenseTypeExpense {
<p class="font-bold text-destructive">
- { fmt.Sprintf("$%.2f", float64(re.AmountCents)/100.0) }
- { model.FormatMoney(re.Amount) }
</p>
} else {
<p class="font-bold text-green-500">
+ { fmt.Sprintf("$%.2f", float64(re.AmountCents)/100.0) }
+ { model.FormatMoney(re.Amount) }
</p>
}
// Toggle pause/resume
@ -352,7 +352,7 @@ templ EditRecurringForm(spaceID string, re *model.RecurringExpenseWithTagsAndMet
Name: "amount",
ID: "edit-recurring-amount-" + re.ID,
Type: "number",
Value: fmt.Sprintf("%.2f", float64(re.AmountCents)/100.0),
Value: model.FormatDecimal(re.Amount),
Attributes: templ.Attributes{"step": "0.01", "required": "true"},
})
</div>

View file

@ -6,9 +6,10 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/moneyaccount"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
"github.com/shopspring/decimal"
)
templ SpaceAccountsPage(space *model.Space, accounts []model.MoneyAccountWithBalance, totalBalance int, availableBalance int, transfers []*model.AccountTransferWithAccount, currentPage, totalPages int) {
templ SpaceAccountsPage(space *model.Space, accounts []model.MoneyAccountWithBalance, totalBalance decimal.Decimal, availableBalance decimal.Decimal, transfers []*model.AccountTransferWithAccount, currentPage, totalPages int) {
@layouts.Space("Accounts", space) {
<div class="space-y-4">
<div class="flex justify-between items-center">

View file

@ -159,14 +159,14 @@ templ BudgetCard(spaceID string, b *model.BudgetWithSpent, tags []*model.Tag) {
// Progress bar
<div class="space-y-1">
<div class="flex justify-between text-sm">
<span>{ fmt.Sprintf("$%.2f", float64(b.SpentCents)/100.0) } spent</span>
<span>of { fmt.Sprintf("$%.2f", float64(b.AmountCents)/100.0) }</span>
<span>{ model.FormatMoney(b.Spent) } spent</span>
<span>of { model.FormatMoney(b.Amount) }</span>
</div>
<div class="w-full bg-muted rounded-full h-2.5">
<div class={ "h-2.5 rounded-full transition-all", progressBarColor(b.Status) } style={ fmt.Sprintf("width: %.1f%%", pct) }></div>
</div>
if b.Status == model.BudgetStatusOver {
<p class="text-xs text-destructive font-medium">Over budget by { fmt.Sprintf("$%.2f", float64(b.SpentCents-b.AmountCents)/100.0) }</p>
<p class="text-xs text-destructive font-medium">Over budget by { model.FormatMoney(b.Spent.Sub(b.Amount)) }</p>
}
</div>
</div>
@ -311,7 +311,7 @@ templ EditBudgetForm(spaceID string, b *model.BudgetWithSpent, tags []*model.Tag
Name: "amount",
ID: "edit-budget-amount-" + b.ID,
Type: "number",
Value: fmt.Sprintf("%.2f", float64(b.AmountCents)/100.0),
Value: model.FormatDecimal(b.Amount),
Attributes: templ.Attributes{"step": "0.01", "required": "true"},
})
</div>

View file

@ -3,6 +3,7 @@ package pages
import (
"fmt"
"strconv"
"github.com/shopspring/decimal"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
@ -14,7 +15,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/blocks/dialogs"
)
templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTagsAndMethod, balance int, allocated int, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems, methods []*model.PaymentMethod, currentPage, totalPages int) {
templ SpaceExpensesPage(space *model.Space, expenses []*model.ExpenseWithTagsAndMethod, balance decimal.Decimal, allocated decimal.Decimal, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems, methods []*model.PaymentMethod, currentPage, totalPages int) {
@layouts.Space("Expenses", space) {
<div class="space-y-4">
<div class="flex justify-between items-center">
@ -121,11 +122,11 @@ templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
<div class="flex items-center gap-1 shrink-0">
if exp.Type == model.ExpenseTypeExpense {
<p class="font-bold text-destructive">
- { fmt.Sprintf("$%.2f", float64(exp.AmountCents)/100.0) }
- { model.FormatMoney(exp.Amount) }
</p>
} else {
<p class="font-bold text-green-500">
+ { fmt.Sprintf("$%.2f", float64(exp.AmountCents)/100.0) }
+ { model.FormatMoney(exp.Amount) }
</p>
}
// Edit button
@ -186,12 +187,12 @@ templ ExpenseListItem(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
</div>
}
templ ExpenseCreatedResponse(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, balance int, allocated int, tags []*model.Tag, currentPage, totalPages int) {
templ ExpenseCreatedResponse(spaceID string, expenses []*model.ExpenseWithTagsAndMethod, balance decimal.Decimal, allocated decimal.Decimal, tags []*model.Tag, currentPage, totalPages int) {
@ExpensesListContent(spaceID, expenses, nil, tags, currentPage, totalPages)
@expense.BalanceCard(spaceID, balance, allocated, true)
}
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTagsAndMethod, balance int, allocated int, methods []*model.PaymentMethod, tags []*model.Tag) {
templ ExpenseUpdatedResponse(spaceID string, exp *model.ExpenseWithTagsAndMethod, balance decimal.Decimal, allocated decimal.Decimal, methods []*model.PaymentMethod, tags []*model.Tag) {
@ExpenseListItem(spaceID, exp, methods, tags)
@expense.BalanceCard(exp.SpaceID, balance, allocated, true)
}

View file

@ -3,6 +3,7 @@ package pages
import (
"fmt"
"strconv"
"github.com/shopspring/decimal"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
@ -17,7 +18,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
)
templ SpaceLoanDetailPage(space *model.Space, loan *model.LoanWithPaymentSummary, receipts []*model.ReceiptWithSourcesAndAccounts, currentPage, totalPages int, recurringReceipts []*model.RecurringReceiptWithSources, accounts []model.MoneyAccountWithBalance, availableBalance int) {
templ SpaceLoanDetailPage(space *model.Space, loan *model.LoanWithPaymentSummary, receipts []*model.ReceiptWithSourcesAndAccounts, currentPage, totalPages int, recurringReceipts []*model.RecurringReceiptWithSources, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
@layouts.Space(loan.Name, space) {
<div class="space-y-6">
// Loan Summary Card
@ -94,8 +95,8 @@ templ SpaceLoanDetailPage(space *model.Space, loan *model.LoanWithPaymentSummary
templ LoanSummaryCard(spaceID string, loan *model.LoanWithPaymentSummary) {
{{ progressPct := 0 }}
if loan.OriginalAmountCents > 0 {
{{ progressPct = (loan.TotalPaidCents * 100) / loan.OriginalAmountCents }}
if !loan.OriginalAmount.IsZero() {
{{ progressPct = int(loan.TotalPaid.Div(loan.OriginalAmount).Mul(decimal.NewFromInt(100)).IntPart()) }}
if progressPct > 100 {
{{ progressPct = 100 }}
}
@ -154,17 +155,17 @@ templ LoanSummaryCard(spaceID string, loan *model.LoanWithPaymentSummary) {
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-sm text-muted-foreground">Original</p>
<p class="text-lg font-semibold">{ fmt.Sprintf("$%.2f", float64(loan.OriginalAmountCents)/100.0) }</p>
<p class="text-lg font-semibold">{ model.FormatMoney(loan.OriginalAmount) }</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Paid</p>
<p class="text-lg font-semibold text-green-600">{ fmt.Sprintf("$%.2f", float64(loan.TotalPaidCents)/100.0) }</p>
<p class="text-lg font-semibold text-green-600">{ model.FormatMoney(loan.TotalPaid) }</p>
</div>
<div>
<p class="text-sm text-muted-foreground">Remaining</p>
<p class="text-lg font-semibold">
if loan.RemainingCents > 0 {
{ fmt.Sprintf("$%.2f", float64(loan.RemainingCents)/100.0) }
if loan.Remaining.GreaterThan(decimal.Zero) {
{ model.FormatMoney(loan.Remaining) }
} else {
$0.00
}
@ -244,7 +245,7 @@ templ ReceiptListItem(spaceID, loanID string, receipt *model.ReceiptWithSourcesA
<div id={ "receipt-" + receipt.ID } class="p-4 flex justify-between items-start">
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="font-medium">{ fmt.Sprintf("$%.2f", float64(receipt.TotalAmountCents)/100.0) }</span>
<span class="font-medium">{ model.FormatMoney(receipt.TotalAmount) }</span>
<span class="text-sm text-muted-foreground">{ receipt.Date.Format("Jan 2, 2006") }</span>
if receipt.RecurringReceiptID != nil {
@icon.Repeat(icon.Props{Class: "size-3 text-muted-foreground"})
@ -257,11 +258,11 @@ templ ReceiptListItem(spaceID, loanID string, receipt *model.ReceiptWithSourcesA
for _, src := range receipt.Sources {
if src.SourceType == "balance" {
@badge.Badge(badge.Props{Variant: badge.VariantSecondary, Class: "text-xs"}) {
{ fmt.Sprintf("Balance $%.2f", float64(src.AmountCents)/100.0) }
{ fmt.Sprintf("Balance %s", model.FormatMoney(src.Amount)) }
}
} else {
@badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "text-xs"}) {
{ fmt.Sprintf("%s $%.2f", src.AccountName, float64(src.AmountCents)/100.0) }
{ fmt.Sprintf("%s %s", src.AccountName, model.FormatMoney(src.Amount)) }
}
}
}
@ -304,7 +305,7 @@ templ RecurringReceiptItem(spaceID, loanID string, rr *model.RecurringReceiptWit
<div class="space-y-1">
<div class="flex items-center gap-2">
@icon.Repeat(icon.Props{Class: "size-4"})
<span class="font-medium">{ fmt.Sprintf("$%.2f", float64(rr.TotalAmountCents)/100.0) }</span>
<span class="font-medium">{ model.FormatMoney(rr.TotalAmount) }</span>
<span class="text-sm text-muted-foreground">{ string(rr.Frequency) }</span>
if !rr.IsActive {
@badge.Badge(badge.Props{Variant: badge.VariantSecondary}) {
@ -322,12 +323,12 @@ templ RecurringReceiptItem(spaceID, loanID string, rr *model.RecurringReceiptWit
for _, src := range rr.Sources {
if src.SourceType == "balance" {
@badge.Badge(badge.Props{Variant: badge.VariantSecondary, Class: "text-xs"}) {
{ fmt.Sprintf("Balance $%.2f", float64(src.AmountCents)/100.0) }
{ fmt.Sprintf("Balance %s", model.FormatMoney(src.Amount)) }
}
} else {
@badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "text-xs"}) {
if src.AccountID != nil {
{ fmt.Sprintf("Account $%.2f", float64(src.AmountCents)/100.0) }
{ fmt.Sprintf("Account %s", model.FormatMoney(src.Amount)) }
}
}
}
@ -379,7 +380,7 @@ templ RecurringReceiptItem(spaceID, loanID string, rr *model.RecurringReceiptWit
</div>
}
templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance int) {
templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
<form
hx-post={ fmt.Sprintf("/app/spaces/%s/loans/%s/receipts", spaceID, loanID) }
hx-swap="none"
@ -423,7 +424,7 @@ templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWit
<div class="space-y-2">
<label class="text-sm font-medium">Funding Sources</label>
<p class="text-xs text-muted-foreground">
Available balance: { fmt.Sprintf("$%.2f", float64(availableBalance)/100.0) }
Available balance: { model.FormatMoney(availableBalance) }
</p>
<div id="funding-sources" class="space-y-2">
<div class="flex gap-2 items-center source-row">
@ -431,7 +432,7 @@ templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWit
<option value="balance">General Balance</option>
for _, acct := range accounts {
<option value="account" data-account-id={ acct.ID }>
{ acct.Name } ({ fmt.Sprintf("$%.2f", float64(acct.BalanceCents)/100.0) })
{ acct.Name } ({ model.FormatMoney(acct.Balance) })
</option>
}
</select>
@ -483,7 +484,7 @@ templ CreateReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWit
</script>
}
templ CreateRecurringReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance int) {
templ CreateRecurringReceiptForm(spaceID, loanID string, accounts []model.MoneyAccountWithBalance, availableBalance decimal.Decimal) {
<form
hx-post={ fmt.Sprintf("/app/spaces/%s/loans/%s/recurring", spaceID, loanID) }
hx-swap="none"
@ -547,7 +548,7 @@ templ CreateRecurringReceiptForm(spaceID, loanID string, accounts []model.MoneyA
<div class="space-y-2">
<label class="text-sm font-medium">Funding Sources</label>
<p class="text-xs text-muted-foreground">
Current balance: { fmt.Sprintf("$%.2f", float64(availableBalance)/100.0) }
Current balance: { model.FormatMoney(availableBalance) }
</p>
<div id="recurring-funding-sources" class="space-y-2">
<div class="flex gap-2 items-center recurring-source-row">
@ -555,7 +556,7 @@ templ CreateRecurringReceiptForm(spaceID, loanID string, accounts []model.MoneyA
<option value="balance">General Balance</option>
for _, acct := range accounts {
<option value="account" data-account-id={ acct.ID }>
{ acct.Name } ({ fmt.Sprintf("$%.2f", float64(acct.BalanceCents)/100.0) })
{ acct.Name } ({ model.FormatMoney(acct.Balance) })
</option>
}
</select>

View file

@ -3,6 +3,7 @@ package pages
import (
"fmt"
"strconv"
"github.com/shopspring/decimal"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
@ -185,8 +186,8 @@ templ LoansListContent(spaceID string, loans []*model.LoanWithPaymentSummary, cu
templ LoanCard(spaceID string, loan *model.LoanWithPaymentSummary) {
{{ progressPct := 0 }}
if loan.OriginalAmountCents > 0 {
{{ progressPct = (loan.TotalPaidCents * 100) / loan.OriginalAmountCents }}
if !loan.OriginalAmount.IsZero() {
{{ progressPct = int(loan.TotalPaid.Div(loan.OriginalAmount).Mul(decimal.NewFromInt(100)).IntPart()) }}
if progressPct > 100 {
{{ progressPct = 100 }}
}
@ -205,7 +206,7 @@ templ LoanCard(spaceID string, loan *model.LoanWithPaymentSummary) {
}
</div>
@card.Description() {
{ fmt.Sprintf("$%.2f", float64(loan.OriginalAmountCents)/100.0) }
{ model.FormatMoney(loan.OriginalAmount) }
if loan.InterestRateBps > 0 {
{ fmt.Sprintf(" @ %.2f%%", float64(loan.InterestRateBps)/100.0) }
}
@ -218,9 +219,9 @@ templ LoanCard(spaceID string, loan *model.LoanWithPaymentSummary) {
Class: "h-2",
})
<div class="flex justify-between text-sm text-muted-foreground mt-2">
<span>Paid: { fmt.Sprintf("$%.2f", float64(loan.TotalPaidCents)/100.0) }</span>
if loan.RemainingCents > 0 {
<span>Left: { fmt.Sprintf("$%.2f", float64(loan.RemainingCents)/100.0) }</span>
<span>Paid: { model.FormatMoney(loan.TotalPaid) }</span>
if loan.Remaining.GreaterThan(decimal.Zero) {
<span>Left: { model.FormatMoney(loan.Remaining) }</span>
} else {
<span class="text-green-600">Fully paid</span>
}

View file

@ -3,6 +3,7 @@ package pages
import (
"fmt"
"sort"
"github.com/shopspring/decimal"
"git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
@ -14,8 +15,8 @@ import (
type OverviewData struct {
Space *model.Space
Balance int
Allocated int
Balance decimal.Decimal
Allocated decimal.Decimal
Report *model.SpendingReport
Budgets []*model.BudgetWithSpent
UpcomingRecurring []*model.RecurringExpenseWithTagsAndMethod
@ -143,12 +144,12 @@ templ overviewBalanceCard(data OverviewData) {
<h3 class="font-semibold mb-3">Current Balance</h3>
@dialogs.AddTransaction(data.Space, data.Tags, data.ListsWithItems, data.Methods)
</div>
<p class={ "text-3xl font-bold", templ.KV("text-destructive", data.Balance < 0) }>
{ fmt.Sprintf("$%.2f", float64(data.Balance)/100.0) }
<p class={ "text-3xl font-bold", templ.KV("text-destructive", data.Balance.IsNegative()) }>
{ model.FormatMoney(data.Balance) }
</p>
if data.Allocated > 0 {
if data.Allocated.GreaterThan(decimal.Zero) {
<p class="text-sm text-muted-foreground mt-1">
{ fmt.Sprintf("$%.2f", float64(data.Allocated)/100.0) } in accounts
{ model.FormatMoney(data.Allocated) } in accounts
</p>
}
</div>
@ -161,17 +162,17 @@ templ overviewSpendingCard(data OverviewData) {
<div class="space-y-1">
<div class="flex justify-between">
<span class="text-sm text-green-500 font-medium">Income</span>
<span class="text-sm font-bold text-green-500">{ fmt.Sprintf("$%.2f", float64(data.Report.TotalIncome)/100.0) }</span>
<span class="text-sm font-bold text-green-500">{ model.FormatMoney(data.Report.TotalIncome) }</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-destructive font-medium">Expenses</span>
<span class="text-sm font-bold text-destructive">{ fmt.Sprintf("$%.2f", float64(data.Report.TotalExpenses)/100.0) }</span>
<span class="text-sm font-bold text-destructive">{ model.FormatMoney(data.Report.TotalExpenses) }</span>
</div>
<hr class="border-border"/>
<div class="flex justify-between">
<span class="text-sm font-medium">Net</span>
<span class={ "text-sm font-bold", templ.KV("text-green-500", data.Report.NetBalance >= 0), templ.KV("text-destructive", data.Report.NetBalance < 0) }>
{ fmt.Sprintf("$%.2f", float64(data.Report.NetBalance)/100.0) }
<span class={ "text-sm font-bold", templ.KV("text-green-500", !data.Report.NetBalance.IsNegative()), templ.KV("text-destructive", data.Report.NetBalance.IsNegative()) }>
{ model.FormatMoney(data.Report.NetBalance) }
</span>
</div>
</div>
@ -191,7 +192,7 @@ templ overviewSpendingByCategoryChart(data OverviewData) {
tagColors := make([]string, len(data.Report.ByTag))
for i, t := range data.Report.ByTag {
tagLabels[i] = t.TagName
tagData[i] = float64(t.TotalAmount) / 100.0
tagData[i] = t.TotalAmount.InexactFloat64()
tagColors[i] = overviewChartColor(i, t.TagColor)
}
}}
@ -224,7 +225,7 @@ templ overviewSpendingOverTimeChart(data OverviewData) {
var timeData []float64
for _, d := range data.Report.DailySpending {
timeLabels = append(timeLabels, d.Date.Format("Jan 02"))
timeData = append(timeData, float64(d.TotalCents)/100.0)
timeData = append(timeData, d.Total.InexactFloat64())
}
}}
@chart.Chart(chart.Props{
@ -276,8 +277,8 @@ templ overviewBudgetsCard(data OverviewData) {
<span class="text-xs text-muted-foreground ml-auto">{ overviewPeriodLabel(b.Period) }</span>
</div>
<div class="flex justify-between text-xs text-muted-foreground">
<span>{ fmt.Sprintf("$%.2f", float64(b.SpentCents)/100.0) } spent</span>
<span>of { fmt.Sprintf("$%.2f", float64(b.AmountCents)/100.0) }</span>
<span>{ model.FormatMoney(b.Spent) } spent</span>
<span>of { model.FormatMoney(b.Amount) }</span>
</div>
<div class="w-full bg-muted rounded-full h-2">
<div class={ "h-2 rounded-full transition-all", overviewProgressBarColor(b.Status) } style={ fmt.Sprintf("width: %.1f%%", pct) }></div>
@ -309,11 +310,11 @@ templ overviewRecurringCard(data OverviewData) {
</div>
if r.Type == model.ExpenseTypeExpense {
<p class="text-sm font-bold text-destructive shrink-0">
{ fmt.Sprintf("$%.2f", float64(r.AmountCents)/100.0) }
{ model.FormatMoney(r.Amount) }
</p>
} else {
<p class="text-sm font-bold text-green-500 shrink-0">
+{ fmt.Sprintf("$%.2f", float64(r.AmountCents)/100.0) }
+{ model.FormatMoney(r.Amount) }
</p>
}
</div>
@ -348,7 +349,7 @@ templ overviewTopExpensesCard(data OverviewData) {
}
</div>
<p class="text-sm font-bold text-destructive shrink-0">
{ fmt.Sprintf("$%.2f", float64(exp.AmountCents)/100.0) }
{ model.FormatMoney(exp.Amount) }
</p>
</div>
}

View file

@ -9,6 +9,7 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/chart"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
"github.com/shopspring/decimal"
)
var defaultChartColors = []string{
@ -72,17 +73,17 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T
<div class="space-y-1">
<div class="flex justify-between">
<span class="text-green-500 font-medium">Income</span>
<span class="font-bold text-green-500">{ fmt.Sprintf("$%.2f", float64(report.TotalIncome)/100.0) }</span>
<span class="font-bold text-green-500">{ model.FormatMoney(report.TotalIncome) }</span>
</div>
<div class="flex justify-between">
<span class="text-destructive font-medium">Expenses</span>
<span class="font-bold text-destructive">{ fmt.Sprintf("$%.2f", float64(report.TotalExpenses)/100.0) }</span>
<span class="font-bold text-destructive">{ model.FormatMoney(report.TotalExpenses) }</span>
</div>
<hr class="border-border"/>
<div class="flex justify-between">
<span class="font-medium">Net</span>
<span class={ "font-bold", templ.KV("text-green-500", report.NetBalance >= 0), templ.KV("text-destructive", report.NetBalance < 0) }>
{ fmt.Sprintf("$%.2f", float64(report.NetBalance)/100.0) }
<span class={ "font-bold", templ.KV("text-green-500", report.NetBalance.GreaterThanOrEqual(decimal.Zero)), templ.KV("text-destructive", report.NetBalance.LessThan(decimal.Zero)) }>
{ model.FormatMoney(report.NetBalance) }
</span>
</div>
</div>
@ -97,7 +98,7 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T
tagColors := make([]string, len(report.ByTag))
for i, t := range report.ByTag {
tagLabels[i] = t.TagName
tagData[i] = float64(t.TotalAmount) / 100.0
tagData[i] = t.TotalAmount.InexactFloat64()
tagColors[i] = chartColor(i, t.TagColor)
}
}}
@ -133,12 +134,12 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T
if days <= 31 {
for _, d := range report.DailySpending {
timeLabels = append(timeLabels, d.Date.Format("Jan 02"))
timeData = append(timeData, float64(d.TotalCents)/100.0)
timeData = append(timeData, d.Total.InexactFloat64())
}
} else {
for _, m := range report.MonthlySpending {
timeLabels = append(timeLabels, m.Month)
timeData = append(timeData, float64(m.TotalCents)/100.0)
timeData = append(timeData, m.Total.InexactFloat64())
}
}
}}
@ -189,7 +190,7 @@ templ ReportCharts(spaceID string, report *model.SpendingReport, from, to time.T
}
</div>
<p class="font-bold text-destructive text-sm shrink-0">
{ fmt.Sprintf("$%.2f", float64(exp.AmountCents)/100.0) }
{ model.FormatMoney(exp.Amount) }
</p>
</div>
}