fix: migrate dashboard to space handler & rm dashboard handler

This commit is contained in:
juancwu 2026-02-18 15:03:20 +00:00
commit d6b9eb9c59
4 changed files with 38 additions and 132 deletions

View file

@ -1,59 +0,0 @@
package handler
import (
"fmt"
"log/slog"
"net/http"
"strings"
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
"git.juancwu.dev/juancwu/budgit/internal/service"
"git.juancwu.dev/juancwu/budgit/internal/ui"
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
)
type dashboardHandler struct {
spaceService *service.SpaceService
expenseService *service.ExpenseService
}
func NewDashboardHandler(ss *service.SpaceService, es *service.ExpenseService) *dashboardHandler {
return &dashboardHandler{
spaceService: ss,
expenseService: es,
}
}
func (h *dashboardHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
spaces, err := h.spaceService.GetSpacesForUser(user.ID)
if err != nil {
slog.Error("failed to get spaces for user", "error", err, "user_id", user.ID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
ui.Render(w, r, pages.Dashboard(spaces))
}
func (h *dashboardHandler) CreateSpace(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
w.Header().Set("HX-Reswap", "none")
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprint(w, `<p id="create-space-error" hx-swap-oob="true" class="text-sm text-destructive">Space name is required</p>`)
return
}
space, err := h.spaceService.CreateSpace(name, user.ID)
if err != nil {
slog.Error("failed to create space", "error", err, "user_id", user.ID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", "/app/spaces/"+space.ID)
w.WriteHeader(http.StatusOK)
}

View file

@ -1,70 +0,0 @@
package handler
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"git.juancwu.dev/juancwu/budgit/internal/repository"
"git.juancwu.dev/juancwu/budgit/internal/service"
"git.juancwu.dev/juancwu/budgit/internal/testutil"
"github.com/stretchr/testify/assert"
)
func TestDashboardHandler_DashboardPage(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
expenseRepo := repository.NewExpenseRepository(dbi.DB)
spaceSvc := service.NewSpaceService(spaceRepo)
expenseSvc := service.NewExpenseService(expenseRepo)
h := NewDashboardHandler(spaceSvc, expenseSvc)
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
testutil.CreateTestSpace(t, dbi.DB, user.ID, "My Space")
req := testutil.NewAuthenticatedRequest(t, http.MethodGet, "/app/dashboard", user, profile, nil)
w := httptest.NewRecorder()
h.DashboardPage(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
}
func TestDashboardHandler_CreateSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
expenseRepo := repository.NewExpenseRepository(dbi.DB)
spaceSvc := service.NewSpaceService(spaceRepo)
expenseSvc := service.NewExpenseService(expenseRepo)
h := NewDashboardHandler(spaceSvc, expenseSvc)
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {"New Space"}})
w := httptest.NewRecorder()
h.CreateSpace(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.True(t, strings.HasPrefix(w.Header().Get("HX-Redirect"), "/app/spaces/"))
})
}
func TestDashboardHandler_CreateSpace_EmptyName(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
spaceRepo := repository.NewSpaceRepository(dbi.DB)
expenseRepo := repository.NewExpenseRepository(dbi.DB)
spaceSvc := service.NewSpaceService(spaceRepo)
expenseSvc := service.NewExpenseService(expenseRepo)
h := NewDashboardHandler(spaceSvc, expenseSvc)
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {""}})
w := httptest.NewRecorder()
h.CreateSpace(w, req)
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
})
}

View file

@ -5,6 +5,7 @@ import (
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
@ -49,6 +50,40 @@ func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *serv
}
}
func (h *SpaceHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
spaces, err := h.spaceService.GetSpacesForUser(user.ID)
if err != nil {
slog.Error("failed to get spaces for user", "error", err, "user_id", user.ID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
ui.Render(w, r, pages.Dashboard(spaces))
}
func (h *SpaceHandler) CreateSpace(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
w.Header().Set("HX-Reswap", "none")
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprint(w, `<p id="create-space-error" hx-swap-oob="true" class="text-sm text-destructive">Space name is required</p>`)
return
}
space, err := h.spaceService.CreateSpace(name, user.ID)
if err != nil {
slog.Error("failed to create space", "error", err, "user_id", user.ID)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", "/app/spaces/"+space.ID)
w.WriteHeader(http.StatusOK)
}
// getExpenseForSpace fetches an expense and verifies it belongs to the given space.
func (h *SpaceHandler) getExpenseForSpace(w http.ResponseWriter, spaceID, expenseID string) *model.Expense {
exp, err := h.expenseService.GetExpense(expenseID)

View file

@ -13,7 +13,6 @@ import (
func SetupRoutes(a *app.App) http.Handler {
auth := handler.NewAuthHandler(a.AuthService, a.InviteService, a.SpaceService)
home := handler.NewHomeHandler()
dashboard := handler.NewDashboardHandler(a.SpaceService, a.ExpenseService)
settings := handler.NewSettingsHandler(a.AuthService, a.UserService)
space := handler.NewSpaceHandler(a.SpaceService, a.TagService, a.ShoppingListService, a.ExpenseService, a.InviteService, a.MoneyAccountService, a.PaymentMethodService, a.RecurringExpenseService, a.BudgetService, a.ReportService)
@ -55,8 +54,9 @@ func SetupRoutes(a *app.App) http.Handler {
mux.HandleFunc("GET /auth/onboarding", middleware.RequireAuth(auth.OnboardingPage))
mux.Handle("POST /auth/onboarding", crudLimiter(http.HandlerFunc(middleware.RequireAuth(auth.CompleteOnboarding))))
mux.HandleFunc("GET /app/dashboard", middleware.RequireAuth(dashboard.DashboardPage))
mux.Handle("POST /app/spaces", crudLimiter(http.HandlerFunc(middleware.RequireAuth(dashboard.CreateSpace))))
mux.HandleFunc("GET /app/dashboard", middleware.Redirect("/app/spaces"))
mux.HandleFunc("GET /app/spaces", middleware.RequireAuth(space.DashboardPage))
mux.Handle("POST /app/spaces", crudLimiter(middleware.RequireAuth(space.CreateSpace)))
mux.HandleFunc("GET /app/settings", middleware.RequireAuth(settings.SettingsPage))
mux.HandleFunc("POST /app/settings/password", authRateLimiter(middleware.RequireAuth(settings.SetPassword)))