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) }

} - - +
+ + +
+
+ @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) }

}