From ff5ed3ec14dba663b8dcfb23f04e425a8fe4f3b7 Mon Sep 17 00:00:00 2001
From: Tim Wundenberg
Date: Sun, 15 Jun 2025 22:02:45 +0200
Subject: [PATCH] feat(dashboard): #163 first summary
---
internal/handler/dashboard.go | 65 -------------------
internal/handler/root_and_404.go | 22 +++++--
internal/service/dashboard.go | 31 +++++----
internal/template/account/account.templ | 11 +---
internal/template/dashboard/dashboard.templ | 35 ++++++++--
.../template/transaction/transaction.templ | 11 ++--
.../transaction_recurring.templ | 11 ++--
.../treasurechest/treasure_chest.templ | 13 +---
internal/types/format.go | 37 +++++++++++
9 files changed, 112 insertions(+), 124 deletions(-)
delete mode 100644 internal/handler/dashboard.go
create mode 100644 internal/types/format.go
diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go
deleted file mode 100644
index 5175aa8..0000000
--- a/internal/handler/dashboard.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package handler
-
-import (
- "fmt"
- "net/http"
- "spend-sparrow/internal/handler/middleware"
- "spend-sparrow/internal/service"
- t "spend-sparrow/internal/template/dashboard"
- "spend-sparrow/internal/utils"
- "time"
-)
-
-type Dashboard interface {
- Handle(router *http.ServeMux)
-}
-
-type DashboardImpl struct {
- s service.Dashboard
- r *Render
-}
-
-func NewDashboard(s service.Dashboard, r *Render) Dashboard {
- return DashboardImpl{
- s: s,
- r: r,
- }
-}
-
-func (h DashboardImpl) Handle(r *http.ServeMux) {
- r.Handle("GET /transaction", h.handleDashboardPage())
-}
-
-func (h DashboardImpl) handleDashboardPage() http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- updateSpan(r)
-
- user := middleware.GetUser(r)
- if user == nil {
- utils.DoRedirect(w, r, "/auth/signin")
- return
- }
-
- var month time.Time
- var err error
- monthStr := r.URL.Query().Get("month")
- if monthStr == "" {
- month, err = time.Parse("2006-01-02", r.FormValue("timestamp"))
- if err != nil {
- handleError(w, r, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest))
- return
- }
- } else {
- month = time.Now()
- }
-
- summary, err := h.s.Summary(r.Context(), user, month)
-
- comp := t.Dashboard(summary)
- if utils.IsHtmx(r) {
- h.r.Render(r, w, comp)
- } else {
- h.r.RenderLayout(r, w, comp, user)
- }
- }
-}
diff --git a/internal/handler/root_and_404.go b/internal/handler/root_and_404.go
index 2b24751..59f8fd5 100644
--- a/internal/handler/root_and_404.go
+++ b/internal/handler/root_and_404.go
@@ -9,6 +9,7 @@ import (
"spend-sparrow/internal/template"
"spend-sparrow/internal/template/dashboard"
"spend-sparrow/internal/types"
+ "spend-sparrow/internal/utils"
"time"
"github.com/a-h/templ"
@@ -41,6 +42,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
user := middleware.GetUser(r)
+ htmx := utils.IsHtmx(r)
+
var comp templ.Component
var status int
@@ -50,7 +53,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
} else {
if user != nil {
var err error
- comp, err = handler.dashboard(user, r)
+ comp, err = handler.dashboard(user, htmx, r)
if err != nil {
slog.Error("Failed to get dashboard summary", "err", err)
}
@@ -60,17 +63,21 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
status = http.StatusOK
}
- handler.r.RenderLayoutWithStatus(r, w, comp, user, status)
+ if htmx {
+ handler.r.RenderWithStatus(r, w, comp, status)
+ } else {
+ handler.r.RenderLayoutWithStatus(r, w, comp, user, status)
+ }
}
}
-func (handler IndexImpl) dashboard(user *types.User, r *http.Request) (templ.Component, error) {
+func (handler IndexImpl) dashboard(user *types.User, htmx bool, r *http.Request) (templ.Component, error) {
var month time.Time
var err error
monthStr := r.URL.Query().Get("month")
if monthStr != "" {
- month, err = time.Parse("2006-01-02", r.FormValue("timestamp"))
+ month, err = time.Parse("2006-01-02", monthStr)
if err != nil {
return nil, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest)
}
@@ -83,8 +90,11 @@ func (handler IndexImpl) dashboard(user *types.User, r *http.Request) (templ.Com
return nil, err
}
- comp := dashboard.Dashboard(summary)
- return comp, nil
+ if htmx {
+ return dashboard.DashboardData(summary), nil
+ } else {
+ return dashboard.Dashboard(summary), nil
+ }
}
func (handler IndexImpl) handleEmpty() http.HandlerFunc {
diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go
index 6c40207..ec896e9 100644
--- a/internal/service/dashboard.go
+++ b/internal/service/dashboard.go
@@ -62,21 +62,24 @@ func (s Dashboard) Summary(ctx context.Context, user *types.User, month time.Tim
summary.Income = *value
}
- // err = s.db.GetContext(ctx, &summary.Expenses, `
- // SELECT SUM(value)
- // FROM "transaction"
- // WHERE user_id = $1
- // AND account_id IS NOT NULL
- // AND treasure_chest_id IS NOT NULL
- // AND error IS NULL
- // AND date(timestamp, 'start of month') = date($2, 'start of month')`,
- // user.Id, month)
- // err = db.TransformAndLogDbError("dashboard", nil, err)
- // if err != nil {
- // return nil, err
- // }
+ err = s.db.GetContext(ctx, &value, `
+ SELECT SUM(value)
+ FROM "transaction"
+ WHERE user_id = $1
+ AND account_id IS NOT NULL
+ AND treasure_chest_id IS NOT NULL
+ AND error IS NULL
+ AND date(timestamp, 'start of month') = date($2, 'start of month')`,
+ user.Id, month)
+ err = db.TransformAndLogDbError("dashboard", nil, err)
+ if err != nil {
+ return nil, err
+ }
+ if value != nil {
+ summary.Expenses = *value
+ }
- summary.Total = summary.Income - summary.Expenses
+ summary.Total = summary.Income + summary.Expenses
summary.Month = month
slog.Info("Dashboard summary", "summary", summary)
diff --git a/internal/template/account/account.templ b/internal/template/account/account.templ
index f0706ae..3a3006f 100644
--- a/internal/template/account/account.templ
+++ b/internal/template/account/account.templ
@@ -1,6 +1,5 @@
package account
-import "fmt"
import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types"
@@ -81,9 +80,9 @@ templ AccountItem(account *types.Account) {
{ account.Name }
if account.CurrentBalance < 0 {
-
{ displayBalance(account.CurrentBalance) }
+
{ types.FormatEuros(account.CurrentBalance) }
} else {
-
{ displayBalance(account.CurrentBalance) }
+
{ types.FormatEuros(account.CurrentBalance) }
}
}
-
-func displayBalance(balance int64) string {
-
- euros := float64(balance) / 100
- return fmt.Sprintf("%.2f €", euros)
-}
diff --git a/internal/template/dashboard/dashboard.templ b/internal/template/dashboard/dashboard.templ
index 6474ffe..3997ec7 100644
--- a/internal/template/dashboard/dashboard.templ
+++ b/internal/template/dashboard/dashboard.templ
@@ -4,18 +4,41 @@ import "spend-sparrow/internal/types"
templ Dashboard(summary *types.DashboardMonthlySummary) {
-
-
+
+
+ @DashboardData(summary)
+
+
+}
+
+templ DashboardData(summary *types.DashboardMonthlySummary) {
+
Savings
Income
Expenses
Total
- { summary.Savings }
- { summary.Income }
- { summary.Expenses }
- { summary.Total }
+ { types.FormatEuros(summary.Savings) }
+ @balance(summary.Income)
+ @balance(summary.Expenses)
+ @balance(summary.Total)
}
+
+templ balance(balance int64) {
+ if balance < 0 {
+ { types.FormatEuros(balance) }
+ } else {
+ { types.FormatEuros(balance) }
+ }
+}
diff --git a/internal/template/transaction/transaction.templ b/internal/template/transaction/transaction.templ
index 1c72c5a..8197799 100644
--- a/internal/template/transaction/transaction.templ
+++ b/internal/template/transaction/transaction.templ
@@ -103,7 +103,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
if transaction.TreasureChestId != nil {
treasureChestId = transaction.TreasureChestId.String()
}
- value = displayBalance(transaction.Value)
+ value = formatFloat(transaction.Value)
id = transaction.Id.String()
cancelUrl = "/transaction/" + id
@@ -250,9 +250,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
if transaction.Value < 0 {
- { displayBalance(transaction.Value)+" €" }
+ { types.FormatEuros(transaction.Value) }
} else {
- { displayBalance(transaction.Value)+" €" }
+ { types.FormatEuros(transaction.Value) }
}