FloorVisualizer/internal/handler/product_handler.go

197 lines
6.2 KiB
Go
Raw Normal View History

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)
variants, _ := repository.GetVariantsByStyle(db, sku)
if len(variants) > 0 {
product.Variants = variants
}
// Check favorite status and build specs
userID := middleware.UserID(r.Context())
favMap, _ := repository.GetFavoritedSKUs(db, userID, []string{sku}) // ok to ignore err: non-critical
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) // ok to ignore err: non-critical
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})
}
}