package dashboard import ( "fmt" "log/slog" "net/http" "spend-sparrow/internal/core" "spend-sparrow/internal/dashboard/template" "spend-sparrow/internal/treasure_chest" "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 treasure_chest.Service } func NewHandler(r *core.Render, s *Service, treasureChest treasure_chest.Service) 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": "" }, "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": "" }, "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) } } }