diff --git a/app/api/openapi.json b/app/api/openapi.json index 6e1d907..4f689bc 100644 --- a/app/api/openapi.json +++ b/app/api/openapi.json @@ -585,6 +585,41 @@ } } }, + "/api/v1/restricted/meili-search/settings": { + "get": { + "tags": ["Search"], + "summary": "Get MeiliSearch settings", + "description": "Returns MeiliSearch configuration and settings. Requires authentication.", + "operationId": "getMeiliSearchSettings", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Settings retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/api/v1/public/auth/me": { "get": { "tags": ["Auth"], @@ -1191,17 +1226,12 @@ } } }, - "/api/v1/restricted/menu/get-routes": { + "/api/v1/public/menu/get-routes": { "get": { "tags": ["Menu"], "summary": "Get routes", - "description": "Returns the routing structure for the current language. Requires authentication.", + "description": "Returns the routing structure for the current language.", "operationId": "getRoutes", - "security": [ - { - "CookieAuth": [] - } - ], "responses": { "200": { "description": "Routes retrieved successfully", @@ -1222,16 +1252,6 @@ } } } - }, - "401": { - "description": "Not authenticated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } } } } diff --git a/app/delivery/web/api/public/routing.go b/app/delivery/web/api/public/routing.go new file mode 100644 index 0000000..9e36274 --- /dev/null +++ b/app/delivery/web/api/public/routing.go @@ -0,0 +1,45 @@ +package public + +import ( + "git.ma-al.com/goc_daniel/b2b/app/service/menuService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" +) + +type RoutingHandler struct { + menuService *menuService.MenuService +} + +func NewRoutingHandler() *RoutingHandler { + menuService := menuService.New() + return &RoutingHandler{ + menuService: menuService, + } +} + +// AuthHandlerRoutes registers all auth routes +func RoutingHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewRoutingHandler() + + r.Get("/get-routes", handler.GetRouting) + + return r +} + +func (h *RoutingHandler) GetRouting(c fiber.Ctx) error { + lang_id, ok := c.Locals("langID").(uint) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + menu, err := h.menuService.GetRoutes(lang_id) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index f8004d8..ee7e615 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -24,7 +24,6 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMenuHandler() r.Get("/get-menu", handler.GetMenu) - r.Get("/get-routes", handler.GetRouting) r.Get("/get-top-menu", handler.GetTopMenu) return r @@ -45,21 +44,6 @@ func (h *MenuHandler) GetMenu(c fiber.Ctx) error { return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK))) } -func (h *MenuHandler) GetRouting(c fiber.Ctx) error { - lang_id, ok := c.Locals("langID").(uint) - if !ok { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) - } - menu, err := h.menuService.GetRoutes(lang_id) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK))) -} - func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { lang_id, ok := c.Locals("langID").(uint) if !ok { diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 94c02a6..f4ecda8 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -89,6 +89,9 @@ func (s *Server) Setup() error { auth := s.public.Group("/auth") public.AuthHandlerRoutes(auth) + menuRouting := s.public.Group("/menu") + public.RoutingHandlerRoutes(menuRouting) + // product translation routes (restricted) productTranslation := s.restricted.Group("/product-translation") restricted.ProductTranslationHandlerRoutes(productTranslation) diff --git a/app/model/productDescription.go b/app/model/productDescription.go index a2c7466..29208a6 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -1,5 +1,7 @@ package model +import "encoding/json" + // ProductDescription contains all the information visible on webpage, in given language. type ProductDescription struct { ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"` @@ -28,20 +30,30 @@ type ProductRow struct { } type MeiliSearchProduct struct { - ProductID uint `gorm:"column:id_product"` - Name string `gorm:"column:name"` - Description string `gorm:"column:description"` - DescriptionShort string `gorm:"column:description_short"` - Usage string `gorm:"column:used_for"` - EAN13 string `gorm:"column:ean13"` - Reference string `gorm:"column:reference"` - Width float64 `gorm:"column:width"` - Height float64 `gorm:"column:height"` - Depth float64 `gorm:"column:depth"` - Weight float64 `gorm:"column:weight"` - CategoryID uint `gorm:"column:id_category"` - CategoryName string `gorm:"column:category_name"` - Variations uint `gorm:"column:variations"` - CategoryIDs []uint `gorm:"-"` // All category IDs including children for filtering - CoverImage string `gorm:"-"` // Cover image URL (not indexed) + ProductID uint `gorm:"column:id_product" json:"product_id"` + Name string `gorm:"column:name" json:"name"` + Active uint8 `gorm:"column:active" json:"active"` + Description string `gorm:"column:description" json:"description"` + DescriptionShort string `gorm:"column:description_short" json:"description_short"` + Usage string `gorm:"column:used_for" json:"usage"` + EAN13 string `gorm:"column:ean13" json:"ean13"` + Reference string `gorm:"column:reference" json:"reference"` + Price float64 `gorm:"column:price" json:"price"` + CategoryID uint `gorm:"column:id_category" json:"category_id"` + CategoryName string `gorm:"column:category_name" json:"category_name"` + Variations uint `gorm:"column:variations" json:"variations"` + + // JSON fields stored as raw, converted to string for search + Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"` + Features json.RawMessage `gorm:"column:features" json:"features"` + AttributeFilters json.RawMessage `gorm:"column:attribute_filters" json:"attribute_filters"` + + // String versions for searchable text (populated during indexing) + FeaturesStr string `json:"features_str"` + AttributesStr string `json:"attributes_str"` + AttributeFiltersStr string `json:"attribute_filters_str"` + + CategoryIDs json.RawMessage `gorm:"column:category_ids" json:"category_ids"` // All category IDs including children for filtering + IDImage uint `gorm:"column:id_image" json:"id_image"` // Cover image ID (not indexed) + CoverImage string `json:"cover_image"` // Cover image URL (populated during indexing) } diff --git a/app/model/routing.go b/app/model/routing.go index 135f427..59090f6 100644 --- a/app/model/routing.go +++ b/app/model/routing.go @@ -1,19 +1,13 @@ package model type Route struct { - ID uint `gorm:"primaryKey;autoIncrement"` - Name string `gorm:"type:varchar(255);not null;unique"` - Path *string `gorm:"type:varchar(255);default:null"` - Component string `gorm:"type:varchar(255);not null;comment:path to component file"` - Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"` - Meta *string `gorm:"type:longtext;default:'{}'"` - IsActive *bool `gorm:"type:tinyint;default:1"` - SortOrder *int `gorm:"type:int;default:0"` - - ParentID *uint `gorm:"index"` - Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"` - - Children []Route `gorm:"foreignKey:ParentID"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"type:varchar(255);not null;unique" json:"name"` + Path *string `gorm:"type:varchar(255);default:null" json:"path,omitempty"` + Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"` + Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"` + Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"` + SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"` } func (Route) TableName() string { diff --git a/app/model/topMenu.go b/app/model/topMenu.go index 5147781..29e2aa4 100644 --- a/app/model/topMenu.go +++ b/app/model/topMenu.go @@ -6,7 +6,7 @@ type B2BTopMenu struct { MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"` Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"` ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"` - Params string `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"` + Params json.RawMessage `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"` Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"` Position int `gorm:"column:position;not null;default:1" json:"position"` diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index 8879779..dd9fc5a 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -9,9 +9,9 @@ import ( ) type UIProductDescriptionRepo interface { - GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) - CreateIfDoesNotExist(productID uint, productLangID uint) error - UpdateFields(productID uint, productLangID uint, updates map[string]string) error + GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error) + CreateIfDoesNotExist(productID uint, productid_lang uint) error + UpdateFields(productID uint, productid_lang uint, updates map[string]string) error GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) } @@ -22,12 +22,12 @@ func New() UIProductDescriptionRepo { } // We assume that any user has access to all product descriptions -func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) { +func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error) { var ProductDescription model.ProductDescription err := db.DB. Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang). First(&ProductDescription).Error if err != nil { return nil, fmt.Errorf("database error: %w", err) @@ -37,16 +37,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLa } // If it doesn't exist, returns an error. -func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error { +func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error { record := model.ProductDescription{ ProductID: productID, ShopID: constdata.SHOP_ID, - LangID: productLangID, + LangID: productid_lang, } err := db.DB. Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang). FirstOrCreate(&record).Error if err != nil { return fmt.Errorf("database error: %w", err) @@ -55,7 +55,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLan return nil } -func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error { +func (r *ProductDescriptionRepo) UpdateFields(productID uint, productid_lang uint, updates map[string]string) error { if len(updates) == 0 { return nil } @@ -66,7 +66,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint err := db.DB. Table("ps_product_lang"). - Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productid_lang). Updates(updatesIface).Error if err != nil { return fmt.Errorf("database error: %w", err) @@ -79,96 +79,123 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) { var products []model.MeiliSearchProduct - err := db.DB.Debug(). - // Select(` - // ps.id_product AS id_product, - // pl.name AS name, - // ps.price AS price, - // pl.description AS description, - // pl.description_short AS description_short, - // pl.usage AS used_for, - // p.ean13 AS ean13, - // p.reference AS reference, - // p.width AS width, - // p.height AS height, - // p.depth AS depth, - // p.weight AS weight, - // ps.id_category_default AS id_category, - // cl.name AS category_name, - // COUNT(DISTINCT pas.id_product_attribute) AS variations - // `). - // Table("ps_product_shop AS ps"). - // Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). - // Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). - // Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). - // Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). - // Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID). - // Group("ps.id_product"). - - Select(` - ps.id_product AS id_product, - pl.name AS name, - pl.description AS description, - pl.description_short AS description_short, - pl.usage AS used_for, - p.ean13 AS ean13, - p.reference AS reference, + query := db.Get().Debug().Raw(` + WITH products_page AS ( + SELECT ps.id_product, ps.price + FROM ps_product_shop ps + WHERE ps.id_shop = ? AND ps.active = 1 + ), + variation_attributes AS ( + SELECT pas.id_product, pagl.public_name AS attribute_name, + JSON_ARRAYAGG(DISTINCT pal.name) AS attribute_values + FROM ps_product_attribute_shop pas + JOIN ps_product_attribute_combination ppac + ON ppac.id_product_attribute = pas.id_product_attribute + JOIN ps_attribute_lang pal + ON pal.id_attribute = ppac.id_attribute AND pal.id_lang = ? + JOIN ps_attribute pa + ON pa.id_attribute = ppac.id_attribute + JOIN ps_attribute_group pag + ON pag.id_attribute_group = pa.id_attribute_group + JOIN ps_attribute_group_lang pagl + ON pagl.id_attribute_group = pag.id_attribute_group AND pagl.id_lang = ? + WHERE pas.id_shop = ? + GROUP BY pas.id_product, pagl.public_name + ), + variations AS ( + SELECT id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes + FROM variation_attributes + GROUP BY id_product + ), + variation_attribute_filters AS ( + SELECT pas.id_product, + JSON_ARRAYAGG( + DISTINCT CONCAT( + LOWER(REPLACE(CAST(pagl.public_name AS CHAR) COLLATE utf8mb4_unicode_ci, ' ', '_')), + ':', + LOWER(REPLACE(CAST(pal.name AS CHAR) COLLATE utf8mb4_unicode_ci, ' ', '_')) + ) + ) AS attribute_filters + FROM ps_product_attribute_shop pas + JOIN ps_product_attribute_combination ppac + ON ppac.id_product_attribute = pas.id_product_attribute + JOIN ps_attribute_lang pal + ON pal.id_attribute = ppac.id_attribute AND pal.id_lang = ? + JOIN ps_attribute pa + ON pa.id_attribute = ppac.id_attribute + JOIN ps_attribute_group pag + ON pag.id_attribute_group = pa.id_attribute_group + JOIN ps_attribute_group_lang pagl + ON pagl.id_attribute_group = pag.id_attribute_group AND pagl.id_lang = ? + WHERE pas.id_shop = ? + GROUP BY pas.id_product + ), + features AS ( + SELECT pfp.id_product, JSON_OBJECTAGG(pfl.name, pfvl.value) AS features + FROM ps_feature_product pfp + JOIN ps_feature_lang pfl + ON pfl.id_feature = pfp.id_feature AND pfl.id_lang = ? + JOIN ps_feature_value_lang pfvl + ON pfvl.id_feature_value = pfp.id_feature_value AND pfvl.id_lang = ? + GROUP BY pfp.id_product + ), + images AS ( + SELECT id_product, id_image + FROM ps_image_shop + WHERE id_shop = ? AND cover = 1 + ), + categories AS ( + SELECT id_product, JSON_ARRAYAGG(id_category) AS category_ids + FROM ps_category_product + GROUP BY id_product + ) + SELECT pp.id_product, + pl.name, + TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description, + TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description_short, + TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.usage, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS used_for, + p.ean13, + p.reference, + pp.price, ps.id_category_default AS id_category, cl.name AS category_name, - cl.link_rewrite as link_rewrite, - COUNT(DISTINCT pas.id_product_attribute) AS variations, - pis.id_image AS id_image, - GROUP_CONCAT(DISTINCT pcp.id_category) AS category_ids - `). - Table("ps_product_shop AS ps"). - Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). - Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). - Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). - Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). - Joins("JOIN ps_image_shop AS pis ON pis.id_product = ps.id_product AND pis.cover = 1"). - Joins("JOIN ps_category_product AS pcp ON pcp.id_product = ps.id_product"). - Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID). - Group("pcp.id_product, ps.id_product"). - Scan(&products).Error - if err != nil { + cl.link_rewrite, + COALESCE(vary.attributes, JSON_OBJECT()) AS attributes, + COALESCE(vaf.attribute_filters, JSON_ARRAY()) AS attribute_filters, + COALESCE(feat.features, JSON_OBJECT()) AS features, + img.id_image, + cat.category_ids, + (SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = pp.id_product AND pas2.id_shop = ?) AS variations + FROM products_page pp + JOIN ps_product_shop ps ON ps.id_product = pp.id_product + JOIN ps_product_lang pl + ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ? + JOIN ps_product p ON p.id_product = ps.id_product + JOIN ps_category_lang cl + ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ? + LEFT JOIN variations vary ON vary.id_product = ps.id_product + LEFT JOIN variation_attribute_filters vaf ON vaf.id_product = ps.id_product + LEFT JOIN features feat ON feat.id_product = ps.id_product + LEFT JOIN images img ON img.id_product = ps.id_product + LEFT JOIN categories cat ON cat.id_product = ps.id_product + ORDER BY ps.id_product + `, + constdata.SHOP_ID, // products_page + id_lang, id_lang, // variation_attributes pal.id_lang, pagl.id_lang + constdata.SHOP_ID, // variation_attributes pas.id_shop + id_lang, id_lang, // variation_attribute_filters pal.id_lang, pagl.id_lang + constdata.SHOP_ID, // variation_attribute_filters pas.id_shop + id_lang, id_lang, // features pfl.id_lang, pfvl.id_lang + constdata.SHOP_ID, // images id_shop + constdata.SHOP_ID, // variation count subquery + constdata.SHOP_ID, // ps_product_lang pl.id_shop + id_lang, // ps_product_lang pl.id_lang + constdata.SHOP_ID, // ps_category_lang cl.id_shop + id_lang, // ps_category_lang cl.id_lang + ) + if err := query.Scan(&products).Error; err != nil { return products, fmt.Errorf("database error: %w", err) } - // Get all category IDs for each product (including child categories) - for i := range products { - var categoryIDs []uint - // Find all parent categories and their children using nested set - err := db.DB. - Table("ps_category AS c"). - Select("c.id_category"). - Joins("JOIN ps_category_product AS cp ON cp.id_category = c.id_category"). - Joins("JOIN ps_category AS parent ON c.nleft >= parent.nleft AND c.nright <= parent.nright AND parent.id_category = ?", products[i].CategoryID). - Where("cp.id_product = ?", products[i].ProductID). - Group("c.id_category"). - Pluck("c.id_category", &categoryIDs).Error - if err != nil { - continue // Skip if error, use default category - } - if len(categoryIDs) > 0 { - products[i].CategoryIDs = categoryIDs - } else { - products[i].CategoryIDs = []uint{products[i].CategoryID} - } - - // Get cover image for the product - var imageID int - err = db.DB. - Table("ps_image AS i"). - Select("i.id_image"). - Joins("LEFT JOIN ps_image_shop AS iss ON iss.id_image = i.id_image AND iss.id_shop = ?", constdata.SHOP_ID). - Where("i.id_product = ? AND (i.cover = 1 OR i.cover IS TRUE)", products[i].ProductID). - Order("i.position ASC"). - Limit(1). - Pluck("i.id_image", &imageID).Error - if err == nil && imageID > 0 { - products[i].CoverImage = fmt.Sprintf("%d/%d.jpg", products[i].ProductID, imageID) - } - } - return products, nil } diff --git a/app/repos/routesRepo/routesRepo.go b/app/repos/routesRepo/routesRepo.go index afcf084..d12d488 100644 --- a/app/repos/routesRepo/routesRepo.go +++ b/app/repos/routesRepo/routesRepo.go @@ -3,6 +3,7 @@ package routesrepo import ( "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" ) type UIRoutesRepo interface { @@ -18,7 +19,7 @@ func New() UIRoutesRepo { func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { routes := []model.Route{} - err := db.DB.Find(&routes).Error + err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error if err != nil { return nil, err } diff --git a/app/service/meiliService/meiliService.go b/app/service/meiliService/meiliService.go index 1fdbe39..b2e4f72 100644 --- a/app/service/meiliService/meiliService.go +++ b/app/service/meiliService/meiliService.go @@ -3,7 +3,6 @@ package meiliService import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -31,81 +30,145 @@ func New() *MeiliService { } } +func getIndexName(id_lang uint) string { + return fmt.Sprintf("shop_%d_lang_%d", constdata.SHOP_ID, id_lang) +} + // ==================================== FOR TESTING ONLY ==================================== func (s *MeiliService) CreateIndex(id_lang uint) error { - indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + indexName := getIndexName(id_lang) products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang) - for i := 0; i < len(products); i++ { - products[i].Description = cleanHTML(products[i].Description) - products[i].DescriptionShort = cleanHTML(products[i].DescriptionShort) - products[i].Usage = cleanHTML(products[i].Usage) + if err != nil { + return fmt.Errorf("failed to get products: %w", err) } - primaryKey := "ProductID" + if len(products) == 0 { + return nil + } + + // Process products: prepare for indexing + for i := range products { + // Convert JSON fields to searchable strings + if len(products[i].Features) > 0 { + products[i].FeaturesStr = string(products[i].Features) + } + if len(products[i].Attributes) > 0 { + products[i].AttributesStr = string(products[i].Attributes) + } + if len(products[i].AttributeFilters) > 0 { + products[i].AttributeFiltersStr = string(products[i].AttributeFilters) + } + + // Build cover image URL from image ID + if products[i].IDImage > 0 { + products[i].CoverImage = config.Get().Image.ImagePrefix + fmt.Sprintf("/%d", products[i].IDImage) + } + } + + // Add documents to index + primaryKey := "product_id" docOptions := &meilisearch.DocumentOptions{ PrimaryKey: &primaryKey, SkipCreation: false, } + task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions) if err != nil { - return fmt.Errorf("meili AddDocuments error: %w", err) + return fmt.Errorf("failed to add documents: %w", err) } - finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) - fmt.Printf("Task status: %s\n", finishedTask.Status) - fmt.Printf("Task error: %s\n", finishedTask.Error) + finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond) + if err != nil { + return fmt.Errorf("failed to wait for task: %w", err) + } + if finishedTask.Status == "failed" { + return fmt.Errorf("task failed: %v", finishedTask.Error) + } + + // Configure filterable attributes filterableAttributes := []interface{}{ - "CategoryID", - "CategoryIDs", + "product_id", + "category_id", + "category_ids", + "active", + "attribute_filters", + "features", } task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes) if err != nil { - return fmt.Errorf("meili AddDocuments error: %w", err) + return fmt.Errorf("failed to update filterable attributes: %w", err) + } + _, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + if err != nil { + return fmt.Errorf("failed to wait for filterable task: %w", err) } - finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) - fmt.Printf("Task status: %s\n", finishedTask.Status) - fmt.Printf("Task error: %s\n", finishedTask.Error) + // Configure sortable attributes + sortableAttributes := []string{ + "price", + "name", + "product_id", + "category_ids", + } + task, err = s.meiliClient.Index(indexName).UpdateSortableAttributes(&sortableAttributes) + if err != nil { + return fmt.Errorf("failed to update sortable attributes: %w", err) + } + _, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + if err != nil { + return fmt.Errorf("failed to wait for sortable task: %w", err) + } + + // Configure displayed attributes displayedAttributes := []string{ - "ProductID", - "Name", - "EAN13", - "Reference", - "Variations", - "CoverImage", + "product_id", + "name", + "ean13", + "reference", + "variations", + "id_image", + "price", + "category_name", + "category_ids", + "attribute_filters", } task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes) if err != nil { - return fmt.Errorf("meili AddDocuments error: %w", err) + return fmt.Errorf("failed to update displayed attributes: %w", err) + } + _, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + if err != nil { + return fmt.Errorf("failed to wait for displayed task: %w", err) } - finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) - fmt.Printf("Task status: %s\n", finishedTask.Status) - fmt.Printf("Task error: %s\n", finishedTask.Error) + // Configure searchable attributes searchableAttributes := []string{ - "Name", - "DescriptionShort", - "Reference", - "EAN13", - "CategoryName", - "Description", - "Usage", + "name", + "description", + "description_short", + "usage", + "features_str", + "attributes_str", + "reference", + "ean13", + "category_name", } task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes) if err != nil { - return fmt.Errorf("meili AddDocuments error: %w", err) + return fmt.Errorf("failed to update searchable attributes: %w", err) + } + _, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + if err != nil { + return fmt.Errorf("failed to wait for searchable task: %w", err) } - finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) - fmt.Printf("Task status: %s\n", finishedTask.Status) - fmt.Printf("Task error: %s\n", finishedTask.Error) - return err + return nil } // ==================================== FOR TESTING ONLY ==================================== func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) { - indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + indexName := getIndexName(id_lang) searchReq := &meilisearch.SearchRequest{ Limit: 4, @@ -128,7 +191,7 @@ func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) { // Search performs a full-text search on the specified index func (s *MeiliService) Search(id_lang uint, query string, id_category uint) (meilisearch.SearchResponse, error) { - indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + indexName := getIndexName(id_lang) filter_query := "Active = 1" if id_category != 0 { @@ -165,7 +228,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) { // GetIndexSettings retrieves the current settings for a specific index func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) { - indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + indexName := getIndexName(id_lang) index := s.meiliClient.Index(indexName) diff --git a/bo/src/components/TopBar.vue b/bo/src/components/TopBar.vue index 13460b7..2c9d8ca 100644 --- a/bo/src/components/TopBar.vue +++ b/bo/src/components/TopBar.vue @@ -3,37 +3,35 @@ import { useFetchJson } from '@/composable/useFetchJson' import LangSwitch from './inner/langSwitch.vue' import ThemeSwitch from './inner/themeSwitch.vue' import { useAuthStore } from '@/stores/auth' -import type { ApiResponse } from '@/types' import { computed, ref } from 'vue' import { currentLang } from '@/router/langs' +import type { LabelTrans, TopMenuItem } from '@/types' +import type { NavigationMenuItem } from '@nuxt/ui' const authStore = useAuthStore() let menu = ref() async function getTopMenu() { try { - const { items } = await useFetchJson('/api/v1/restricted/menu/get-top-menu') + const { items } = await useFetchJson('/api/v1/restricted/menu/get-top-menu') menu.value = items - } catch (err) { - console.log(err); + console.log(err) } } -const menuItems = computed(() => - transformMenu(menu.value[0].children, currentLang.value?.iso_code) -) -function transformMenu(items, locale: string | undefined) { +const menuItems = computed(() => transformMenu(menu.value[0].children, currentLang.value?.iso_code)) +function transformMenu(items: TopMenuItem[], locale: string | undefined): NavigationMenuItem[] { return items.map((item) => { - const parsedLabel = JSON.parse(item.label) - - return { + let route = { icon: 'i-lucide-house', - label: parsedLabel.trans[locale] || parsedLabel.name, - to: { name: parsedLabel.name }, - children: item.children - ? transformMenu(item.children, locale) - : undefined, + label: item.label.trans[locale as keyof LabelTrans].label, + children: item.children ? transformMenu(item.children, locale) : undefined, } + if (item.params?.route) { + route = { ...route, ...{ to: { name: item.params.route.name, params: { locale: locale } } } } + } + + return route }) } @@ -43,8 +41,7 @@ await getTopMenu()