Files
spend-sparrow/internal/dashboard/handler.go
Tim Wundenberg ea2663a53d
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m16s
fix: rename
2025-12-26 07:14:21 +01:00

255 lines
5.7 KiB
Go

package dashboard
import (
"fmt"
"log/slog"
"net/http"
"spend-sparrow/internal/core"
"spend-sparrow/internal/dashboard/template"
"spend-sparrow/internal/service"
"spend-sparrow/internal/utils"
"strings"
"time"
"github.com/google/uuid"
)
type Handler interface {
Handle(router *http.ServeMux)
}
type HandlerImpl struct {
r *core.Render
s *Service
treasureChest service.TreasureChest
}
func NewHandler(r *core.Render, s *Service, treasureChest service.TreasureChest) Handler {
return HandlerImpl{
r: r,
s: s,
treasureChest: treasureChest,
}
}
func (handler HandlerImpl) Handle(router *http.ServeMux) {
router.Handle("GET /dashboard", handler.handleDashboard())
router.Handle("GET /dashboard/main-chart", handler.handleMainChart())
router.Handle("GET /dashboard/treasure-chests", handler.handleTreasureChests())
router.Handle("GET /dashboard/treasure-chest", handler.handleTreasureChest())
}
func (handler HandlerImpl) handleDashboard() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
core.UpdateSpan(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
treasureChests, err := handler.treasureChest.GetAll(r.Context(), user)
if err != nil {
core.HandleError(w, r, err)
return
}
comp := template.Dashboard(treasureChests)
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
}
}
func (handler HandlerImpl) handleMainChart() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
core.UpdateSpan(r)
user := core.GetUser(r)
series, err := handler.s.MainChart(r.Context(), user)
if err != nil {
core.HandleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
accountBuilder := strings.Builder{}
savingsBuilder := strings.Builder{}
for _, entry := range series {
fmt.Fprintf(&accountBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
fmt.Fprintf(&savingsBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100)
}
account := accountBuilder.String()
savings := savingsBuilder.String()
account = account[:len(account)-1]
savings = savings[:len(savings)-1]
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
},
"xAxis": {
"type": "time"
},
"yAxis": {
"axisLabel": {
"formatter": "{value} €"
}
},
"series": [
{
"data": [%s],
"type": "line",
"name": "Account Value"
},
{
"data": [%s],
"type": "line",
"name": "Savings"
}
]
}
`, account, savings)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}
func (handler HandlerImpl) handleTreasureChests() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
core.UpdateSpan(r)
user := core.GetUser(r)
treeList, err := handler.s.TreasureChests(r.Context(), user)
if err != nil {
core.HandleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
dataBuilder := strings.Builder{}
for _, item := range treeList {
childrenBuilder := strings.Builder{}
for _, child := range item.Children {
if child.Value < 0 {
fmt.Fprintf(&childrenBuilder, `{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value)
} else {
fmt.Fprintf(&childrenBuilder, `{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value)
}
}
children := childrenBuilder.String()
children = children[:len(children)-1]
fmt.Fprintf(&dataBuilder, `{"name":"%s","children":[%s]},`, item.Name, children)
}
data := dataBuilder.String()
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)
}
}
}
func (handler HandlerImpl) handleTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
core.UpdateSpan(r)
user := core.GetUser(r)
var treasureChestId *uuid.UUID
treasureChestStr := r.URL.Query().Get("id")
if treasureChestStr != "" {
id, err := uuid.Parse(treasureChestStr)
if err != nil {
core.HandleError(w, r, fmt.Errorf("could not parse treasure chest: %w", core.ErrBadRequest))
return
}
treasureChestId = &id
}
series, err := handler.s.TreasureChest(r.Context(), user, treasureChestId)
if err != nil {
core.HandleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
valueBuilder := strings.Builder{}
for _, entry := range series {
fmt.Fprintf(&valueBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
}
value := valueBuilder.String()
if len(value) > 0 {
value = value[:len(value)-1]
}
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
},
"xAxis": {
"type": "time"
},
"yAxis": {
"axisLabel": {
"formatter": "{value} €"
}
},
"series": [
{
"data": [%s],
"type": "line",
"name": "Treasure Chest Value"
}
]
}
`, value)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}