package handlers import ( "bookhoard/internal/database" "bookhoard/internal/services" "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) // StatsHandler serves the reading-statistics API (/api/stats/*) backed // by the StatisticsService. The server owns every conversion: clients // receive user-timezone-rendered strings plus raw minute/page counts. type StatsHandler struct { statsService *services.StatisticsService } func NewStatsHandler(statsService *services.StatisticsService) *StatsHandler { return &StatsHandler{statsService: statsService} } func mustUser(c *echo.Context) (database.Users, bool) { user, ok := c.Get("user").(database.Users) return user, ok } // Summary handles GET /api/stats/summary?range=7|30|90|year — the /stats // page's complete payload: totals, daily arrays, streaks, most-active // day, top books, per-library breakdown. func (h *StatsHandler) Summary(c *echo.Context) error { user, ok := mustUser(c) if !ok { return echo.NewHTTPError(http.StatusUnauthorized, "invalid user") } summary, err := h.statsService.UserSummary(c.Request().Context(), pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, user.Timezone.String, c.QueryParam("range"), ) if err != nil { if err.Error()[:7] == "invalid" { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, summary) } // ThisWeek handles GET /api/stats/this-week — the dashboard widget's // data: rolling 7-day totals vs the previous seven days, plus streak. // An optional ?library_id scopes the week to one library (the // dashboard's library switcher drives it). func (h *StatsHandler) ThisWeek(c *echo.Context) error { user, ok := mustUser(c) if !ok { return echo.NewHTTPError(http.StatusUnauthorized, "invalid user") } var libraryID pgtype.UUID if raw := c.QueryParam("library_id"); raw != "" { parsed, err := uuid.Parse(raw) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid library id") } libraryID = pgtype.UUID{Bytes: parsed, Valid: true} } week, err := h.statsService.ThisWeek(c.Request().Context(), pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, user.Timezone.String, libraryID, ) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, week) } // BookStats handles GET /api/stats/books/:id — the per-book card on the // book detail page (lifetime totals + recent sessions). func (h *StatsHandler) BookStats(c *echo.Context) error { user, ok := mustUser(c) if !ok { return echo.NewHTTPError(http.StatusUnauthorized, "invalid user") } mediaItemID, err := uuid.Parse(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid media item id") } stats, err := h.statsService.BookStats(c.Request().Context(), pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, mediaItemID, user.Timezone.String, ) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, stats) }