2026-07-16 01:47:11 +00:00
package repository
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"floorvisualizer/internal/model"
)
const selectCols = ` brand , group_name , sku , series_name , style_name ,
category , material , color_tone , finish ,
2026-07-22 07:23:40 +00:00
price_per_sqft , price_tier , price_source , is_official_price ,
2026-07-16 01:47:11 +00:00
main_image_url , room_image_url , source_url , description , status ,
COALESCE ( coverage_sqft_per_box , 0 ) , COALESCE ( wood_species , ' ' ) ,
size_label , COALESCE ( width_in , 0 ) , COALESCE ( length_in , 0 ) `
const insertCols = ` brand , group_name , sku , series_name , style_name ,
category , material , color_tone , finish ,
2026-07-22 07:23:40 +00:00
price_per_sqft , price_tier , price_source , is_official_price ,
2026-07-16 01:47:11 +00:00
main_image_url , room_image_url , source_url , description , status ,
coverage_sqft_per_box , wood_species ,
size_label , width_in , length_in `
// ── Import ──────────────────────────────────────────────────
func InsertProducts ( d * sql . DB , products [ ] model . Product ) error {
for _ , p := range products {
_ , err := d . Exec ( `
2026-07-22 07:23:40 +00:00
INSERT INTO products ( ` +insertCols+ ` ) VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 , $ 8 , $ 9 , $ 10 , $ 11 , $ 12 , $ 13 , $ 14 , $ 15 , $ 16 , $ 17 , $ 18 , $ 19 , $ 20 , $ 21 , $ 22 , $ 23 )
2026-07-16 01:47:11 +00:00
` , p . Brand , p . GroupName , p . SKU , p . SeriesName , p . StyleName ,
p . Category , p . Material , p . ColorTone , p . Finish ,
2026-07-22 07:23:40 +00:00
p . PricePerSqft , p . PriceTier , p . PriceSource , p . IsOfficialPrice ,
2026-07-16 01:47:11 +00:00
p . MainImageURL , p . RoomImageURL , p . SourceURL , p . Description , p . Status ,
p . CoverageSqftPerBox , p . WoodSpecies ,
p . SizeLabel , nf ( p . WidthIn ) , nf ( p . LengthIn ) )
if err != nil {
return fmt . Errorf ( "insert %s: %w" , p . SKU , err )
}
}
return nil
}
func nf ( v float64 ) interface { } {
2026-07-22 07:23:40 +00:00
if v == 0 {
return nil
}
2026-07-16 01:47:11 +00:00
return v
}
// ── Query types ─────────────────────────────────────────────
type FilterParams struct {
SeriesName , Category , Search , Brand , Material , ColorTone , Finish , WoodSpecies string
2026-07-22 07:23:40 +00:00
Page , Limit int
2026-07-16 01:47:11 +00:00
}
type FilterOptions struct {
2026-07-22 07:23:40 +00:00
Categories [ ] string ` json:"categories" `
SeriesNames [ ] string ` json:"series_names" `
2026-07-16 01:47:11 +00:00
Brands [ ] model . BrandInfo ` json:"brands" `
}
// ── Queries ─────────────────────────────────────────────────
func QueryAllProducts ( d * sql . DB ) ( [ ] model . Product , error ) {
prods , _ , err := QueryFilteredProducts ( d , FilterParams { Limit : 10000 } )
return prods , err
}
func QueryFilteredProducts ( d * sql . DB , p FilterParams ) ( [ ] model . Product , int , error ) {
2026-07-22 07:23:40 +00:00
if p . Limit <= 0 {
p . Limit = 20
}
if p . Page <= 0 {
p . Page = 1
}
2026-07-16 01:47:11 +00:00
var conds [ ] string
var args [ ] interface { }
i := 1
add := func ( clause string , val string ) {
if val != "" {
conds = append ( conds , fmt . Sprintf ( clause , i ) )
args = append ( args , val )
i ++
}
}
add ( "category = $%d" , p . Category )
add ( "series_name = $%d" , p . SeriesName )
add ( "brand = $%d" , p . Brand )
add ( "material = $%d" , p . Material )
add ( "color_tone = $%d" , p . ColorTone )
add ( "finish = $%d" , p . Finish )
add ( "wood_species = $%d" , p . WoodSpecies )
if p . Search != "" {
pat := "%" + p . Search + "%"
clauses := [ ] string {
fmt . Sprintf ( "style_name ILIKE $%d" , i ) ,
fmt . Sprintf ( "sku ILIKE $%d" , i + 1 ) ,
fmt . Sprintf ( "brand ILIKE $%d" , i + 2 ) ,
}
conds = append ( conds , "(" + strings . Join ( clauses , " OR " ) + ")" )
2026-07-22 07:23:40 +00:00
for k := 0 ; k < 3 ; k ++ {
args = append ( args , pat )
}
2026-07-16 01:47:11 +00:00
i += 3
}
where := ""
2026-07-22 07:23:40 +00:00
if len ( conds ) > 0 {
where = "WHERE " + strings . Join ( conds , " AND " )
}
2026-07-16 01:47:11 +00:00
// Count distinct styles (dedup same brand+style_name across different sizes)
var total int
d . QueryRow ( "SELECT COUNT(*) FROM (SELECT DISTINCT brand, style_name FROM products " + where + ") AS t" , args ... ) . Scan ( & total )
offset := ( p . Page - 1 ) * p . Limit
// Use DISTINCT ON to keep one row per brand+style_name, pick the first by id
query := fmt . Sprintf ( "SELECT DISTINCT ON (brand, style_name) " + selectCols + " FROM products %s ORDER BY brand, style_name, id LIMIT $%d OFFSET $%d" , where , i , i + 1 )
args = append ( args , p . Limit , offset )
rows , err := d . Query ( query , args ... )
2026-07-22 07:23:40 +00:00
if err != nil {
return nil , 0 , err
}
2026-07-16 01:47:11 +00:00
defer rows . Close ( )
2026-07-22 07:23:40 +00:00
prods , err := ScanProducts ( rows )
2026-07-16 01:47:11 +00:00
return prods , total , err
}
func GetProductBySKU ( d * sql . DB , sku string ) ( * model . Product , error ) {
2026-07-22 07:23:40 +00:00
rows , err := d . Query ( ` SELECT ` + selectCols + ` FROM products WHERE sku = $1 LIMIT 1 ` , sku )
if err != nil {
2026-07-16 01:47:11 +00:00
return nil , err
}
2026-07-22 07:23:40 +00:00
defer rows . Close ( )
prods , err := ScanProducts ( rows )
if err != nil {
return nil , err
}
if len ( prods ) == 0 {
return nil , sql . ErrNoRows
}
return & prods [ 0 ] , nil
2026-07-16 01:47:11 +00:00
}
// GetVariantsByStyle returns all products with the same brand+style_name but different SKU.
func GetVariantsByStyle ( d * sql . DB , sku string ) ( [ ] model . Product , error ) {
rows , err := d . Query ( `
SELECT ` +selectCols+ ` FROM products
WHERE style_name = ( SELECT style_name FROM products WHERE sku = $ 1 )
AND brand = ( SELECT brand FROM products WHERE sku = $ 1 )
AND sku != $ 1
ORDER BY width_in , length_in
` , sku )
2026-07-22 07:23:40 +00:00
if err != nil {
return nil , err
}
2026-07-16 01:47:11 +00:00
defer rows . Close ( )
2026-07-22 07:23:40 +00:00
return ScanProducts ( rows )
2026-07-16 01:47:11 +00:00
}
func VariantCount ( d * sql . DB , sku string ) int {
var n int
d . QueryRow ( ` SELECT COUNT(*) FROM products WHERE style_name = (SELECT style_name FROM products WHERE sku = $1) AND brand = (SELECT brand FROM products WHERE sku = $1) AND sku != $1 ` , sku ) . Scan ( & n )
return n
}
// GetSpecCounts returns (brand|style_name|series_name) → total variant count for a batch of SKUs.
func GetSpecCounts ( d * sql . DB , skus [ ] string ) map [ string ] int {
result := map [ string ] int { }
if len ( skus ) == 0 {
return result
}
placeholders := make ( [ ] string , len ( skus ) )
args := make ( [ ] interface { } , len ( skus ) )
for i , sku := range skus {
placeholders [ i ] = fmt . Sprintf ( "$%d" , i + 1 )
args [ i ] = sku
}
query := fmt . Sprintf ( `
SELECT p . brand , p . style_name , p . series_name , COUNT ( * ) AS total
FROM products p
WHERE ( p . brand , p . style_name , p . series_name ) IN (
SELECT brand , style_name , series_name FROM products WHERE sku IN ( % s )
)
GROUP BY p . brand , p . style_name , p . series_name
` , strings . Join ( placeholders , "," ) )
rows , err := d . Query ( query , args ... )
if err != nil {
return result
}
defer rows . Close ( )
for rows . Next ( ) {
var brand , style , series string
var total int
if err := rows . Scan ( & brand , & style , & series , & total ) ; err == nil {
result [ brand + "|" + style + "|" + series ] = total
}
}
return result
}
func GetFilterOptions ( d * sql . DB ) ( * FilterOptions , error ) {
opts := & FilterOptions { }
2026-07-22 07:23:40 +00:00
for _ , q := range [ ] struct {
query string
out * [ ] string
} {
2026-07-16 01:47:11 +00:00
{ "SELECT DISTINCT category FROM products WHERE category != '' ORDER BY category" , & opts . Categories } ,
{ "SELECT DISTINCT series_name FROM products WHERE series_name != '' ORDER BY series_name" , & opts . SeriesNames } ,
} {
rows , err := d . Query ( q . query )
2026-07-22 07:23:40 +00:00
if err != nil {
return nil , err
}
for rows . Next ( ) {
var v string
rows . Scan ( & v )
* q . out = append ( * q . out , v )
}
2026-07-16 01:47:11 +00:00
rows . Close ( )
}
opts . Brands = GetBrandInfos ( d )
return opts , nil
}
// ── Brand info with stats ───────────────────────────────────
var (
brandLogos map [ string ] string
brandLogosOnce sync . Once
)
func loadBrandLogos ( ) map [ string ] string {
brandLogosOnce . Do ( func ( ) {
brandLogos = map [ string ] string { }
data , err := os . ReadFile ( "brand_logos.json" )
if err != nil {
return
}
json . Unmarshal ( data , & brandLogos )
} )
return brandLogos
}
// GetBrandInfos returns all brands with style count, category count, and logo URL.
func GetBrandInfos ( d * sql . DB ) [ ] model . BrandInfo {
logos := loadBrandLogos ( )
rows , err := d . Query ( `
SELECT brand ,
COUNT ( DISTINCT style_name ) AS style_count ,
COUNT ( DISTINCT SPLIT_PART ( series_name , ' | ' , 1 ) ) AS collection_count
FROM products
WHERE brand != ' '
GROUP BY brand
ORDER BY brand
` )
if err != nil {
return nil
}
defer rows . Close ( )
var out [ ] model . BrandInfo
for rows . Next ( ) {
var bi model . BrandInfo
if err := rows . Scan ( & bi . Name , & bi . StyleCount , & bi . CollectionCount ) ; err != nil {
continue
}
if u , ok := logos [ bi . Name ] ; ok {
bi . LogoURL = u
}
out = append ( out , bi )
}
return out
}
2026-07-22 07:23:40 +00:00
func ScanProducts ( rows * sql . Rows ) ( [ ] model . Product , error ) {
2026-07-16 01:47:11 +00:00
var out [ ] model . Product
for rows . Next ( ) {
var p model . Product
if err := rows . Scan (
& p . Brand , & p . GroupName , & p . SKU , & p . SeriesName , & p . StyleName ,
& p . Category , & p . Material , & p . ColorTone , & p . Finish ,
2026-07-22 07:23:40 +00:00
& p . PricePerSqft , & p . PriceTier , & p . PriceSource , & p . IsOfficialPrice ,
2026-07-16 01:47:11 +00:00
& p . MainImageURL , & p . RoomImageURL , & p . SourceURL , & p . Description , & p . Status ,
& p . CoverageSqftPerBox , & p . WoodSpecies ,
& p . SizeLabel , & p . WidthIn , & p . LengthIn ,
2026-07-22 07:23:40 +00:00
) ; err != nil {
return nil , err
}
2026-07-16 01:47:11 +00:00
out = append ( out , p )
}
return out , rows . Err ( )
}