package handler import ( "context" "database/sql" "net/http" "strconv" "strings" "floorvisualizer/internal/middleware" "floorvisualizer/internal/model" "floorvisualizer/internal/repository" ) type ProductItem struct { Brand string `json:"brand"` SeriesName string `json:"series_name"` StyleName string `json:"style_name"` SKU string `json:"sku"` Category string `json:"category"` Material string `json:"material"` ColorTone string `json:"color_tone"` Finish string `json:"finish"` PricePerSqft float64 `json:"price_per_sqft"` PriceTier string `json:"price_tier"` MainImageURL string `json:"main_image_url"` SizeLabel string `json:"size_label"` CoverageSqftPerBox float64 `json:"coverage_sqft_per_box"` IsFavorited bool `json:"is_favorited"` SpecCount int `json:"spec_count"` } type ProductListResponse struct { Products []ProductItem `json:"products"` Total int `json:"total"` Page int `json:"page"` Limit int `json:"limit"` TotalPages int `json:"total_pages"` FilterOptions *repository.FilterOptions `json:"filter_options"` } var productsCache func(ctx context.Context, sku string) (*model.Product, bool) var productsCacheSet func(ctx context.Context, sku string, product *model.Product) error var productsTrackView func(ctx context.Context, sku string) func SetProductsCache( getter func(ctx context.Context, sku string) (*model.Product, bool), setter func(ctx context.Context, sku string, product *model.Product) error, tracker func(ctx context.Context, sku string), ) { productsCache = getter productsCacheSet = setter productsTrackView = tracker } func Products(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Detail: /products/{sku} sku := strings.TrimPrefix(r.URL.Path, "/products/") if sku != "" && sku != r.URL.Path { // Track view + check cache if productsTrackView != nil { productsTrackView(r.Context(), sku) } var product *model.Product if productsCache != nil { if p, ok := productsCache(r.Context(), sku); ok { product = p } } if product == nil { product, err := repository.GetProductBySKU(db, sku) if err != nil { writeJSON(w, 404, map[string]string{"error": "product not found"}) return } if productsCacheSet != nil { productsCacheSet(r.Context(), sku, product) } } // Attach variants (same style_name, different sizes) if variants, _ := repository.GetVariantsByStyle(db, sku); len(variants) > 0 { product.Variants = variants } // Check favorite status and build specs userID := middleware.UserID(r.Context()) favMap, _ := repository.GetFavoritedSKUs(db, userID, []string{sku}) product.IsFavorited = favMap[sku] // Build specs: main product + variants as size options allVariants := append([]model.Product{*product}, product.Variants...) for _, v := range allVariants { product.Specs = append(product.Specs, model.ProductSpec{ SKU: v.SKU, SizeLabel: v.SizeLabel, WidthIn: v.WidthIn, LengthIn: v.LengthIn, Finish: v.Finish, PricePerSqft: v.PricePerSqft, PriceTier: v.PriceTier, CoverageSqftPerBox: v.CoverageSqftPerBox, MainImageURL: v.MainImageURL, }) } writeJSON(w, 200, product) return } if r.Method != http.MethodGet { writeErr(w, 405, "Method not allowed") return } page, _ := strconv.Atoi(r.URL.Query().Get("page")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) q := r.URL.Query() params := repository.FilterParams{ SeriesName: q.Get("series_name"), Category: q.Get("category"), Material: q.Get("material"), ColorTone: q.Get("color_tone"), Finish: q.Get("finish"), WoodSpecies: q.Get("wood_species"), Search: q.Get("search"), Brand: q.Get("brand"), Page: page, Limit: limit, } prods, total, err := repository.QueryFilteredProducts(db, params) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } filterOpts, _ := repository.GetFilterOptions(db) if filterOpts == nil { filterOpts = &repository.FilterOptions{} } // Collect SKUs for batch favorite check skus := make([]string, len(prods)) for i, p := range prods { skus[i] = p.SKU } userID := middleware.UserID(r.Context()) favSKUs, _ := repository.GetFavoritedSKUs(db, userID, skus) specCounts := repository.GetSpecCounts(db, skus) items := make([]ProductItem, 0, len(prods)) for _, p := range prods { items = append(items, ProductItem{ Brand: p.Brand, SeriesName: p.SeriesName, StyleName: p.StyleName, SKU: p.SKU, Category: p.Category, Material: p.Material, ColorTone: p.ColorTone, Finish: p.Finish, PricePerSqft: p.PricePerSqft, PriceTier: p.PriceTier, MainImageURL: p.MainImageURL, SizeLabel: p.SizeLabel, CoverageSqftPerBox: p.CoverageSqftPerBox, IsFavorited: favSKUs[p.SKU], SpecCount: specCounts[p.Brand+"|"+p.StyleName+"|"+p.SeriesName], }) } effectiveLimit := params.Limit if effectiveLimit <= 0 { effectiveLimit = 20 } totalPages := 0 if total > 0 { totalPages = (total + effectiveLimit - 1) / effectiveLimit } writeJSON(w, 200, ProductListResponse{ Products: items, Total: total, Page: params.Page, Limit: effectiveLimit, TotalPages: totalPages, FilterOptions: filterOpts, }) } } func ProductOptions(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts, err := repository.GetFilterOptions(db) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, opts) } } func FilterOptions(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { filters, err := repository.GetFilterHierarchy(db) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, map[string]interface{}{"filters": filters}) } }