package handler import ( "fmt" "log/slog" "net/http" "spend-sparrow/internal/handler/middleware" "spend-sparrow/internal/service" "spend-sparrow/internal/template/dashboard" "spend-sparrow/internal/utils" "strings" "time" "github.com/google/uuid" ) type Dashboard interface { Handle(router *http.ServeMux) } type DashboardImpl struct { r *Render d *service.Dashboard treasureChest service.TreasureChest } func NewDashboard(r *Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard { return DashboardImpl{ r: r, d: d, treasureChest: treasureChest, } } func (handler DashboardImpl) Handle(router *http.ServeMux) { router.Handle("GET /dashboard", handler.handleDashboard()) router.Handle("GET /dashboard/main-chart", handler.handleDashboardMainChart()) router.Handle("GET /dashboard/treasure-chests", handler.handleDashboardTreasureChests()) router.Handle("GET /dashboard/treasure-chest", handler.handleDashboardTreasureChest()) } func (handler DashboardImpl) handleDashboard() 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 } treasureChests, err := handler.treasureChest.GetAll(r.Context(), user) if err != nil { handleError(w, r, err) return } comp := dashboard.Dashboard(treasureChests) handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK) } } func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { updateSpan(r) user := middleware.GetUser(r) series, err := handler.d.MainChart(r.Context(), user) if err != nil { 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 { accountBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)) savingsBuilder.WriteString(fmt.Sprintf(`["%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 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) dataBuilder := strings.Builder{} for _, item := range treeList { childrenBuilder := strings.Builder{} for _, child := range item.Children { if child.Value < 0 { childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value)) } else { childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value)) } } children := childrenBuilder.String() children = children[:len(children)-1] dataBuilder.WriteString(fmt.Sprintf(`{"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 DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { updateSpan(r) user := middleware.GetUser(r) var treasureChestId *uuid.UUID treasureChestStr := r.URL.Query().Get("id") if treasureChestStr != "" { id, err := uuid.Parse(treasureChestStr) if err != nil { handleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest)) return } treasureChestId = &id } series, err := handler.d.TreasureChest(r.Context(), user, treasureChestId) if err != nil { handleError(w, r, err) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) valueBuilder := strings.Builder{} for _, entry := range series { valueBuilder.WriteString(fmt.Sprintf(`["%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) } } }