From 72869e5c68b2b8568f13f0dfedbbeadd48168876 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Fri, 20 Jun 2025 21:28:47 +0200 Subject: [PATCH] feat(dashboard): #192 include treemap of treasure chests --- internal/handler/dashboard.go | 58 ++++++++++++++++++++- internal/service/dashboard.go | 37 +++++++++++++ internal/service/treasure_chest.go | 4 +- internal/template/dashboard/dashboard.templ | 3 +- internal/types/dashboard.go | 6 +++ static/js/dashboard.js | 44 ++++++++++++---- 6 files changed, 138 insertions(+), 14 deletions(-) diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go index 4d6440d..4710fa4 100644 --- a/internal/handler/dashboard.go +++ b/internal/handler/dashboard.go @@ -29,7 +29,8 @@ func NewDashboard(r *Render, d *service.Dashboard) Dashboard { func (handler DashboardImpl) Handle(router *http.ServeMux) { router.Handle("GET /dashboard", handler.handleDashboard()) - router.Handle("GET /dashboard/dataset", handler.handleDashboardDataset()) + router.Handle("GET /dashboard/main-chart", handler.handleDashboardMainChart()) + router.Handle("GET /dashboard/treasure-chests", handler.handleDashboardTreasureChests()) } func (handler DashboardImpl) handleDashboard() http.HandlerFunc { @@ -47,7 +48,7 @@ func (handler DashboardImpl) handleDashboard() http.HandlerFunc { } } -func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc { +func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { updateSpan(r) @@ -74,6 +75,9 @@ func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc { _, err = fmt.Fprintf(w, ` { + "aria": { + "enabled": true + }, "tooltip": { "trigger": "axis", "formatter": "" @@ -105,3 +109,53 @@ func (handler DashboardImpl) handleDashboardDataset() http.HandlerFunc { } } } + +func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + + user := middleware.GetUser(r) + + treeList, err := handler.d.TreasureChests(r.Context(), user) + if err != nil { + handleError(w, r, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + data := "" + for _, item := range treeList { + children := "" + for _, child := range item.Children { + if child.Value < 0 { + children += fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value) + } else { + children += fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value) + } + } + children = children[:len(children)-1] + data += fmt.Sprintf(`{"name":"%s","children":[%s]},`, item.Name, children) + } + data = data[:len(data)-1] + + _, err = fmt.Fprintf(w, ` + { + "aria": { + "enabled": true + }, + "series": [ + { + "data": [%s], + "type": "treemap", + "name": "Savings" + } + ] + } + `, data) + if err != nil { + slog.InfoContext(r.Context(), "could not write response", "err", err) + } + } +} diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go index 4726434..6f70bae 100644 --- a/internal/service/dashboard.go +++ b/internal/service/dashboard.go @@ -75,3 +75,40 @@ func (s Dashboard) MainChart( return timeEntries, nil } + +func (s Dashboard) TreasureChests( + ctx context.Context, + user *types.User, +) ([]*types.DashboardTreasureChest, error) { + if user == nil { + return nil, ErrUnauthorized + } + + treasureChests := make([]*types.TreasureChest, 0) + err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id) + err = db.TransformAndLogDbError(ctx, "dashboard TreasureChests", nil, err) + if err != nil { + return nil, err + } + + treasureChests = sortTreasureChests(treasureChests) + + result := make([]*types.DashboardTreasureChest, 0) + for _, t := range treasureChests { + if t.ParentId == nil { + result = append(result, &types.DashboardTreasureChest{ + Name: t.Name, + Value: t.CurrentBalance, + Children: make([]types.DashboardTreasureChest, 0), + }) + } else { + result[len(result)-1].Children = append(result[len(result)-1].Children, types.DashboardTreasureChest{ + Name: t.Name, + Value: t.CurrentBalance, + Children: make([]types.DashboardTreasureChest, 0), + }) + } + } + + return result, nil +} diff --git a/internal/service/treasure_chest.go b/internal/service/treasure_chest.go index 645dfbc..54e0ed5 100644 --- a/internal/service/treasure_chest.go +++ b/internal/service/treasure_chest.go @@ -205,7 +205,7 @@ func (s TreasureChestImpl) GetAll(ctx context.Context, user *types.User) ([]*typ return nil, err } - return sortTree(treasureChests), nil + return sortTreasureChests(treasureChests), nil } func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr string) error { @@ -277,7 +277,7 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s return nil } -func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest { +func sortTreasureChests(nodes []*types.TreasureChest) []*types.TreasureChest { var ( roots []*types.TreasureChest ) diff --git a/internal/template/dashboard/dashboard.templ b/internal/template/dashboard/dashboard.templ index 362cf96..5e98fb3 100644 --- a/internal/template/dashboard/dashboard.templ +++ b/internal/template/dashboard/dashboard.templ @@ -2,6 +2,7 @@ package dashboard templ Dashboard() {
-
+
+
} diff --git a/internal/types/dashboard.go b/internal/types/dashboard.go index 013ee93..2a6484b 100644 --- a/internal/types/dashboard.go +++ b/internal/types/dashboard.go @@ -22,3 +22,9 @@ type DashboardMainChartEntry struct { Value int64 Savings int64 } + +type DashboardTreasureChest struct { + Name string + Value int64 + Children []DashboardTreasureChest +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index a53d666..1df405c 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -1,7 +1,7 @@ // Initialize the echarts instance based on the prepared dom -async function init() { - const element = document.getElementById('graph') +async function initMainChart() { + const element = document.getElementById('main-chart') if (element === null) { return; } @@ -9,7 +9,7 @@ async function init() { var myChart = echarts.init(element); try { - const response = await fetch("/dashboard/dataset"); + const response = await fetch("/dashboard/main-chart"); if (!response.ok) { throw new Error(`Response status: ${response.status}`); } @@ -24,15 +24,41 @@ async function init() { const chart = myChart.setOption(option); window.addEventListener('resize', function() { - myChart.resize(); - }); + myChart.resize(); + }); - console.log("initialized charts"); + console.log("initialized main-chart"); } catch (error) { console.error(error.message); } - - // Display the chart using the configuration items and data just specified. } -init(); +async function initTreasureChests() { + const element = document.getElementById('treasure-chests') + if (element === null) { + return; + } + + var myChart = echarts.init(element); + + try { + const response = await fetch("/dashboard/treasure-chests"); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const option = await response.json(); + + const chart = myChart.setOption(option); + window.addEventListener('resize', function() { + myChart.resize(); + }); + + console.log("initialized treasure-chests"); + } catch (error) { + console.error(error.message); + } +} + +initMainChart(); +initTreasureChests();