From 487ea82c3436f3a936791f285347e95eab4e82fd Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Sun, 15 Jun 2025 14:24:18 +0200 Subject: [PATCH] wip --- internal/default.go | 3 +- internal/handler/dashboard.go | 65 ++++++++++++++++ internal/handler/root_and_404.go | 44 +++++++++-- internal/handler/transaction.go | 5 -- internal/service/dashboard.go | 82 +++++++++++++-------- internal/template/dashboard/dashboard.templ | 30 ++++---- internal/types/dashboard.go | 26 ++++++- 7 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 internal/handler/dashboard.go diff --git a/internal/default.go b/internal/default.go index 13ed5db..de0a339 100644 --- a/internal/default.go +++ b/internal/default.go @@ -116,9 +116,10 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { treasureChestService := service.NewTreasureChest(d, randomService, clockService) transactionService := service.NewTransaction(d, randomService, clockService) transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService) + dashboardService := service.NewDashboard(d) render := handler.NewRender() - indexHandler := handler.NewIndex(render) + indexHandler := handler.NewIndex(render, dashboardService) authHandler := handler.NewAuth(authService, render) accountHandler := handler.NewAccount(accountService, render) treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render) diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go new file mode 100644 index 0000000..5175aa8 --- /dev/null +++ b/internal/handler/dashboard.go @@ -0,0 +1,65 @@ +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 5073b31..2b24751 100644 --- a/internal/handler/root_and_404.go +++ b/internal/handler/root_and_404.go @@ -1,10 +1,15 @@ package handler import ( + "fmt" + "log/slog" "net/http" "spend-sparrow/internal/handler/middleware" + "spend-sparrow/internal/service" "spend-sparrow/internal/template" "spend-sparrow/internal/template/dashboard" + "spend-sparrow/internal/types" + "time" "github.com/a-h/templ" ) @@ -14,12 +19,14 @@ type Index interface { } type IndexImpl struct { - render *Render + r *Render + d *service.Dashboard } -func NewIndex(render *Render) Index { +func NewIndex(r *Render, d *service.Dashboard) Index { return IndexImpl{ - render: render, + r: r, + d: d, } } @@ -42,17 +49,44 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { status = http.StatusNotFound } else { if user != nil { - comp = dashboard.Dashboard() + var err error + comp, err = handler.dashboard(user, r) + if err != nil { + slog.Error("Failed to get dashboard summary", "err", err) + } } else { comp = template.Index() } status = http.StatusOK } - handler.render.RenderLayoutWithStatus(r, w, comp, user, status) + handler.r.RenderLayoutWithStatus(r, w, comp, user, status) } } +func (handler IndexImpl) dashboard(user *types.User, 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")) + if err != nil { + return nil, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest) + } + } else { + month = time.Now() + } + + summary, err := handler.d.Summary(r.Context(), user, month) + if err != nil { + return nil, err + } + + comp := dashboard.Dashboard(summary) + return comp, nil +} + func (handler IndexImpl) handleEmpty() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { updateSpan(r) diff --git a/internal/handler/transaction.go b/internal/handler/transaction.go index 9c28028..4caf686 100644 --- a/internal/handler/transaction.go +++ b/internal/handler/transaction.go @@ -14,8 +14,6 @@ import ( "github.com/a-h/templ" "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) type Transaction interface { @@ -56,9 +54,6 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { return } - currentSpan := trace.SpanFromContext(r.Context()) - currentSpan.SetAttributes(attribute.String("", "test")) - filter := types.TransactionItemsFilter{ AccountId: r.URL.Query().Get("account-id"), TreasureChestId: r.URL.Query().Get("treasure-chest-id"), diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go index 51e53d6..6c40207 100644 --- a/internal/service/dashboard.go +++ b/internal/service/dashboard.go @@ -2,6 +2,8 @@ package service import ( "context" + "log/slog" + "spend-sparrow/internal/db" "spend-sparrow/internal/types" "time" @@ -12,50 +14,72 @@ type Dashboard struct { db *sqlx.DB } -func NewDashboard(db *sqlx.DB) TreasureChest { - return TreasureChestImpl{ +func NewDashboard(db *sqlx.DB) *Dashboard { + return &Dashboard{ db: db, } } -func (s Dashboard) Summary(ctx context.Context, user *types.User, month time.Time) (*types.DashboardSummary, error) { +func (s Dashboard) Summary(ctx context.Context, user *types.User, month time.Time) (*types.DashboardMonthlySummary, error) { if user == nil { return nil, ErrUnauthorized } - var summary types.DashboardSummary + var summary types.DashboardMonthlySummary - s.db.SelectContext(ctx, &summary.Expenses, ` - SELECT + var value *int64 + err := s.db.GetContext(ctx, &value, ` + SELECT SUM(value) FROM "transaction" WHERE user_id = $1 - AND value < 0 - AND account_id IS NOT NULL - AND error IS NULL - AND date_trunc('month', date) = date_trund('month', $2)`, - user.Id, month) - - s.db.SelectContext(ctx, &summary.Income, ` - SELECT - FROM "transaction" - WHERE user_id = $1 - AND value > 0 - AND account_id IS NOT NULL - AND treasure_chest_id IS NULL - AND error IS NULL - AND date_trunc('month', date) = date_trund('month', $2)`, - user.Id, month) - - s.db.SelectContext(ctx, &summary.Savings, ` - SELECT - FROM "transaction" - WHERE user_id = $1 - AND value > 0 AND treasure_chest_id IS NOT NULL AND account_id IS NULL AND error IS NULL - AND date_trunc('month', date) = date_trund('month', $2)`, + 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.Savings = *value + } + + 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 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.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 + // } + + summary.Total = summary.Income - summary.Expenses + summary.Month = month + + slog.Info("Dashboard summary", "summary", summary) return &summary, nil } diff --git a/internal/template/dashboard/dashboard.templ b/internal/template/dashboard/dashboard.templ index eb2dae0..6474ffe 100644 --- a/internal/template/dashboard/dashboard.templ +++ b/internal/template/dashboard/dashboard.templ @@ -1,23 +1,21 @@ package dashboard -templ Dashboard() { +import "spend-sparrow/internal/types" + +templ Dashboard(summary *types.DashboardMonthlySummary) {
- @summaryCard() +
+ Savings + Income + Expenses + Total + + { summary.Savings } + { summary.Income } + { summary.Expenses } + { summary.Total } +
} - -templ summaryCard() { -
- Einnahmen - Gespart - Ausgaben - Gesamt - - 4,005.15 € - 4,005.15 € - 3,805.15 € - 200€ -
-} diff --git a/internal/types/dashboard.go b/internal/types/dashboard.go index abe440e..3991699 100644 --- a/internal/types/dashboard.go +++ b/internal/types/dashboard.go @@ -1,8 +1,26 @@ package types -type DashboardSummary struct { - Income int64 - Savings int64 +import "time" + +// Account | TreasureChest | Value | Description +// --------|---------------|-------|---------------- +// Y | Y | + | Invalid +// Y | Y | - | Expense +// Y | N | + | Deposit +// Y | N | - | Withdrawal (for moving between accounts) +// N | Y | + | Saving +// N | Y | - | Withdrawal (for moving between treasure chests) +// N | N | + | Invalid +// N | N | - | Invalid + +type DashboardMonthlySummary struct { + Month time.Time + // Sum of all Transactions with TreasureChests and no Accounts + Savings int64 + // Sum of all Transactions with Accounts and no TreasureChests + Income int64 + // Sum of all Transactions with Accounts and TreasureChests Expenses int64 - Total int64 + // Income - Expenses + Total int64 }