package handlers import ( "bookhoard/internal/database" "bookhoard/internal/services" "bookhoard/internal/utils" "net/http" "strconv" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) type DashboardHandler struct { db *database.Queries dashboardService *services.DashboardService } func NewDashboardHandler(db *database.Queries) *DashboardHandler { return &DashboardHandler{ db: db, dashboardService: services.NewDashboardService(db), } } func (h *DashboardHandler) GetSections(c echo.Context) error { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) libraryID := c.QueryParam("library_id") if libraryID == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"}) } libUUID, err := uuid.Parse(libraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) } prefs, _ := h.dashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID) limit := 20 if limitStr := c.QueryParam("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } } sections, err := h.dashboardService.GetDashboardSections( c.Request().Context(), userUUID, libUUID, limit, prefs.CollectionOrder, prefs.HiddenCollections, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"}) } sectionData := BuildSections(sections) return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData}) } func (h *DashboardHandler) UpdatePreferences(c echo.Context) error { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) var req struct { LibraryID string `json:"library_id"` HiddenCollections []string `json:"hidden_collections"` CollectionOrder []string `json:"collection_order"` ItemsPerSection int `json:"items_per_section"` } if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) } libUUID, err := uuid.Parse(req.LibraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) } prefs, err := h.dashboardService.UpsertDashboardPreferences(c.Request().Context(), database.UpsertDashboardPreferencesParams{ UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, HiddenCollections: req.HiddenCollections, CollectionOrder: req.CollectionOrder, ItemsPerSection: pgtype.Int4{Int32: int32(req.ItemsPerSection), Valid: true}, }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to save preferences"}) } return c.JSON(http.StatusOK, prefs) } func (h *DashboardHandler) RestoreSystemCollection(c echo.Context) error { user := c.Get("user").(database.Users) userUUID := uuid.UUID(user.ID.Bytes) var req struct { CollectionName string `json:"collection_name"` ResetType string `json:"reset_type"` // "full" or "keep_books" } if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) } if req.CollectionName == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "collection_name required"}) } if req.ResetType == "" { req.ResetType = "full" } validCollections := map[string]bool{ "continue-reading": true, "recently-added": true, "recently-read": true, "not-started": true, } if !validCollections[req.CollectionName] { return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid system collection name"}) } if req.ResetType != "full" && req.ResetType != "keep_books" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "reset_type must be 'full' or 'keep_books'"}) } err := h.dashboardService.RestoreSystemCollection(c.Request().Context(), userUUID, req.CollectionName, req.ResetType) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to restore system collection"}) } return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"}) } func BuildSections(sections []services.DashboardSection) []SectionData { var result []SectionData for _, ds := range sections { bookCards := make([]BookInfo, len(ds.Items)) for i, item := range ds.Items { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) bookCards[i] = BookInfo{ MediaItemID: itemUUID.String(), Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), } } result = append(result, SectionData{ ID: ds.CollectionName, IsSystem: ds.IsSystem, Title: ds.Title, Description: ds.Description, Icon: ds.Icon, Items: bookCards, ViewAllURL: getViewAllURL(ds.CollectionName, ds.QueryType), Priority: ds.Priority, }) } return result } func getViewAllURL(key, queryType string) string { urls := map[string]string{ "continue-reading": "/section/continue-reading", "recently-added": "/section/recently-added", "recently-read": "/history", "not-started": "/section/not-started", } if url, exists := urls[queryType]; exists { return url } return "" }