fix meilisearch

This commit is contained in:
2026-03-26 22:00:42 +01:00
parent 29260080c2
commit ec05101037
21 changed files with 545 additions and 294 deletions

View File

@@ -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": { "/api/v1/public/auth/me": {
"get": { "get": {
"tags": ["Auth"], "tags": ["Auth"],
@@ -1191,17 +1226,12 @@
} }
} }
}, },
"/api/v1/restricted/menu/get-routes": { "/api/v1/public/menu/get-routes": {
"get": { "get": {
"tags": ["Menu"], "tags": ["Menu"],
"summary": "Get routes", "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", "operationId": "getRoutes",
"security": [
{
"CookieAuth": []
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Routes retrieved successfully", "description": "Routes retrieved successfully",
@@ -1222,16 +1252,6 @@
} }
} }
} }
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
} }
} }
} }

View File

@@ -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)))
}

View File

@@ -24,7 +24,6 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler() handler := NewMenuHandler()
r.Get("/get-menu", handler.GetMenu) r.Get("/get-menu", handler.GetMenu)
r.Get("/get-routes", handler.GetRouting)
r.Get("/get-top-menu", handler.GetTopMenu) r.Get("/get-top-menu", handler.GetTopMenu)
return r 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))) 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 { func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {

View File

@@ -89,6 +89,9 @@ func (s *Server) Setup() error {
auth := s.public.Group("/auth") auth := s.public.Group("/auth")
public.AuthHandlerRoutes(auth) public.AuthHandlerRoutes(auth)
menuRouting := s.public.Group("/menu")
public.RoutingHandlerRoutes(menuRouting)
// product translation routes (restricted) // product translation routes (restricted)
productTranslation := s.restricted.Group("/product-translation") productTranslation := s.restricted.Group("/product-translation")
restricted.ProductTranslationHandlerRoutes(productTranslation) restricted.ProductTranslationHandlerRoutes(productTranslation)

View File

@@ -1,5 +1,7 @@
package model package model
import "encoding/json"
// ProductDescription contains all the information visible on webpage, in given language. // ProductDescription contains all the information visible on webpage, in given language.
type ProductDescription struct { type ProductDescription struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"` ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
@@ -28,20 +30,30 @@ type ProductRow struct {
} }
type MeiliSearchProduct struct { type MeiliSearchProduct struct {
ProductID uint `gorm:"column:id_product"` ProductID uint `gorm:"column:id_product" json:"product_id"`
Name string `gorm:"column:name"` Name string `gorm:"column:name" json:"name"`
Description string `gorm:"column:description"` Active uint8 `gorm:"column:active" json:"active"`
DescriptionShort string `gorm:"column:description_short"` Description string `gorm:"column:description" json:"description"`
Usage string `gorm:"column:used_for"` DescriptionShort string `gorm:"column:description_short" json:"description_short"`
EAN13 string `gorm:"column:ean13"` Usage string `gorm:"column:used_for" json:"usage"`
Reference string `gorm:"column:reference"` EAN13 string `gorm:"column:ean13" json:"ean13"`
Width float64 `gorm:"column:width"` Reference string `gorm:"column:reference" json:"reference"`
Height float64 `gorm:"column:height"` Price float64 `gorm:"column:price" json:"price"`
Depth float64 `gorm:"column:depth"` CategoryID uint `gorm:"column:id_category" json:"category_id"`
Weight float64 `gorm:"column:weight"` CategoryName string `gorm:"column:category_name" json:"category_name"`
CategoryID uint `gorm:"column:id_category"` Variations uint `gorm:"column:variations" json:"variations"`
CategoryName string `gorm:"column:category_name"`
Variations uint `gorm:"column:variations"` // JSON fields stored as raw, converted to string for search
CategoryIDs []uint `gorm:"-"` // All category IDs including children for filtering Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"`
CoverImage string `gorm:"-"` // Cover image URL (not indexed) 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)
} }

View File

@@ -1,19 +1,13 @@
package model package model
type Route struct { type Route struct {
ID uint `gorm:"primaryKey;autoIncrement"` ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);not null;unique"` Name string `gorm:"type:varchar(255);not null;unique" json:"name"`
Path *string `gorm:"type:varchar(255);default:null"` Path *string `gorm:"type:varchar(255);default:null" json:"path,omitempty"`
Component string `gorm:"type:varchar(255);not null;comment:path to component file"` Component string `gorm:"type:varchar(255);not null;comment:path to component file" json:"component"`
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"` Meta *string `gorm:"type:longtext;default:'{}'" json:"meta,omitempty"`
Meta *string `gorm:"type:longtext;default:'{}'"` Active *bool `gorm:"type:tinyint;default:1" json:"active,omitempty"`
IsActive *bool `gorm:"type:tinyint;default:1"` SortOrder *int `gorm:"type:int;default:0" json:"sort_order,omitempty"`
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"`
} }
func (Route) TableName() string { func (Route) TableName() string {

View File

@@ -6,7 +6,7 @@ type B2BTopMenu struct {
MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"` MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"`
Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"` 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"` 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"` Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"`
Position int `gorm:"column:position;not null;default:1" json:"position"` Position int `gorm:"column:position;not null;default:1" json:"position"`

View File

@@ -9,9 +9,9 @@ import (
) )
type UIProductDescriptionRepo interface { type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productLangID uint) error CreateIfDoesNotExist(productID uint, productid_lang uint) error
UpdateFields(productID uint, productLangID uint, updates map[string]string) error UpdateFields(productID uint, productid_lang uint, updates map[string]string) error
GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, 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 // 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 var ProductDescription model.ProductDescription
err := db.DB. err := db.DB.
Table("ps_product_lang"). 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 First(&ProductDescription).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("database error: %w", err) 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. // 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{ record := model.ProductDescription{
ProductID: productID, ProductID: productID,
ShopID: constdata.SHOP_ID, ShopID: constdata.SHOP_ID,
LangID: productLangID, LangID: productid_lang,
} }
err := db.DB. err := db.DB.
Table("ps_product_lang"). 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 FirstOrCreate(&record).Error
if err != nil { if err != nil {
return fmt.Errorf("database error: %w", err) return fmt.Errorf("database error: %w", err)
@@ -55,7 +55,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLan
return nil 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 { if len(updates) == 0 {
return nil return nil
} }
@@ -66,7 +66,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
err := db.DB. err := db.DB.
Table("ps_product_lang"). 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 Updates(updatesIface).Error
if err != nil { if err != nil {
return fmt.Errorf("database error: %w", err) 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) { func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct var products []model.MeiliSearchProduct
err := db.DB.Debug(). query := db.Get().Debug().Raw(`
// Select(` WITH products_page AS (
// ps.id_product AS id_product, SELECT ps.id_product, ps.price
// pl.name AS name, FROM ps_product_shop ps
// ps.price AS price, WHERE ps.id_shop = ? AND ps.active = 1
// pl.description AS description, ),
// pl.description_short AS description_short, variation_attributes AS (
// pl.usage AS used_for, SELECT pas.id_product, pagl.public_name AS attribute_name,
// p.ean13 AS ean13, JSON_ARRAYAGG(DISTINCT pal.name) AS attribute_values
// p.reference AS reference, FROM ps_product_attribute_shop pas
// p.width AS width, JOIN ps_product_attribute_combination ppac
// p.height AS height, ON ppac.id_product_attribute = pas.id_product_attribute
// p.depth AS depth, JOIN ps_attribute_lang pal
// p.weight AS weight, ON pal.id_attribute = ppac.id_attribute AND pal.id_lang = ?
// ps.id_category_default AS id_category, JOIN ps_attribute pa
// cl.name AS category_name, ON pa.id_attribute = ppac.id_attribute
// COUNT(DISTINCT pas.id_product_attribute) AS variations JOIN ps_attribute_group pag
// `). ON pag.id_attribute_group = pa.id_attribute_group
// Table("ps_product_shop AS ps"). JOIN ps_attribute_group_lang pagl
// 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). ON pagl.id_attribute_group = pag.id_attribute_group AND pagl.id_lang = ?
// Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). WHERE pas.id_shop = ?
// 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). GROUP BY pas.id_product, pagl.public_name
// 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). variations AS (
// Group("ps.id_product"). SELECT id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes
FROM variation_attributes
Select(` GROUP BY id_product
ps.id_product AS id_product, ),
pl.name AS name, variation_attribute_filters AS (
pl.description AS description, SELECT pas.id_product,
pl.description_short AS description_short, JSON_ARRAYAGG(
pl.usage AS used_for, DISTINCT CONCAT(
p.ean13 AS ean13, LOWER(REPLACE(CAST(pagl.public_name AS CHAR) COLLATE utf8mb4_unicode_ci, ' ', '_')),
p.reference AS reference, ':',
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, ps.id_category_default AS id_category,
cl.name AS category_name, cl.name AS category_name,
cl.link_rewrite as link_rewrite, cl.link_rewrite,
COUNT(DISTINCT pas.id_product_attribute) AS variations, COALESCE(vary.attributes, JSON_OBJECT()) AS attributes,
pis.id_image AS id_image, COALESCE(vaf.attribute_filters, JSON_ARRAY()) AS attribute_filters,
GROUP_CONCAT(DISTINCT pcp.id_category) AS category_ids COALESCE(feat.features, JSON_OBJECT()) AS features,
`). img.id_image,
Table("ps_product_shop AS ps"). cat.category_ids,
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). (SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = pp.id_product AND pas2.id_shop = ?) AS variations
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). FROM products_page pp
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). JOIN ps_product_shop ps ON ps.id_product = pp.id_product
Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). JOIN ps_product_lang pl
Joins("JOIN ps_image_shop AS pis ON pis.id_product = ps.id_product AND pis.cover = 1"). ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?
Joins("JOIN ps_category_product AS pcp ON pcp.id_product = ps.id_product"). JOIN ps_product p ON p.id_product = ps.id_product
Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID). JOIN ps_category_lang cl
Group("pcp.id_product, ps.id_product"). ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?
Scan(&products).Error LEFT JOIN variations vary ON vary.id_product = ps.id_product
if err != nil { 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) 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 return products, nil
} }

View File

@@ -3,6 +3,7 @@ package routesrepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "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/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
) )
type UIRoutesRepo interface { type UIRoutesRepo interface {
@@ -18,7 +19,7 @@ func New() UIRoutesRepo {
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
routes := []model.Route{} routes := []model.Route{}
err := db.DB.Find(&routes).Error err := db.DB.Find(&routes, model.Route{Active: nullable.GetNil(true)}).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,7 +3,6 @@ package meiliService
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "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 ==================================== // ==================================== FOR TESTING ONLY ====================================
func (s *MeiliService) CreateIndex(id_lang uint) error { 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) products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
for i := 0; i < len(products); i++ { if err != nil {
products[i].Description = cleanHTML(products[i].Description) return fmt.Errorf("failed to get products: %w", err)
products[i].DescriptionShort = cleanHTML(products[i].DescriptionShort)
products[i].Usage = cleanHTML(products[i].Usage)
} }
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{ docOptions := &meilisearch.DocumentOptions{
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
SkipCreation: false, SkipCreation: false,
} }
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions) task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
if err != nil { 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{}{ filterableAttributes := []interface{}{
"CategoryID", "product_id",
"CategoryIDs", "category_id",
"category_ids",
"active",
"attribute_filters",
"features",
} }
task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes) task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
if err != nil { 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{ displayedAttributes := []string{
"ProductID", "product_id",
"Name", "name",
"EAN13", "ean13",
"Reference", "reference",
"Variations", "variations",
"CoverImage", "id_image",
"price",
"category_name",
"category_ids",
"attribute_filters",
} }
task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes) task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes)
if err != nil { 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{ searchableAttributes := []string{
"Name", "name",
"DescriptionShort", "description",
"Reference", "description_short",
"EAN13", "usage",
"CategoryName", "features_str",
"Description", "attributes_str",
"Usage", "reference",
"ean13",
"category_name",
} }
task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes) task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes)
if err != nil { 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 ==================================== // ==================================== FOR TESTING ONLY ====================================
func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) { 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{ searchReq := &meilisearch.SearchRequest{
Limit: 4, 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 // 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) { 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" filter_query := "Active = 1"
if id_category != 0 { if id_category != 0 {
@@ -165,7 +228,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
// GetIndexSettings retrieves the current settings for a specific index // GetIndexSettings retrieves the current settings for a specific index
func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) { 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) index := s.meiliClient.Index(indexName)

View File

@@ -3,37 +3,35 @@ import { useFetchJson } from '@/composable/useFetchJson'
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/langSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/themeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import type { ApiResponse } from '@/types'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { currentLang } from '@/router/langs' import { currentLang } from '@/router/langs'
import type { LabelTrans, TopMenuItem } from '@/types'
import type { NavigationMenuItem } from '@nuxt/ui'
const authStore = useAuthStore() const authStore = useAuthStore()
let menu = ref() let menu = ref()
async function getTopMenu() { async function getTopMenu() {
try { try {
const { items } = await useFetchJson<ApiResponse>('/api/v1/restricted/menu/get-top-menu') const { items } = await useFetchJson<TopMenuItem>('/api/v1/restricted/menu/get-top-menu')
menu.value = items menu.value = items
} catch (err) { } catch (err) {
console.log(err); console.log(err)
} }
} }
const menuItems = computed(() => const menuItems = computed(() => transformMenu(menu.value[0].children, currentLang.value?.iso_code))
transformMenu(menu.value[0].children, currentLang.value?.iso_code) function transformMenu(items: TopMenuItem[], locale: string | undefined): NavigationMenuItem[] {
)
function transformMenu(items, locale: string | undefined) {
return items.map((item) => { return items.map((item) => {
const parsedLabel = JSON.parse(item.label) let route = {
return {
icon: 'i-lucide-house', icon: 'i-lucide-house',
label: parsedLabel.trans[locale] || parsedLabel.name, label: item.label.trans[locale as keyof LabelTrans].label,
to: { name: parsedLabel.name }, children: item.children ? transformMenu(item.children, locale) : undefined,
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()
<template> <template>
{{ menuItems }} {{ menuItems }}
<!-- fixed top-0 left-0 right-0 z-50 --> <!-- fixed top-0 left-0 right-0 z-50 -->
<header <header class="bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
class=" bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14"> <div class="flex items-center justify-between h-14">
<!-- Logo --> <!-- Logo -->
@@ -55,7 +52,7 @@ await getTopMenu()
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink> </RouterLink>
<UNavigationMenu :items="menuItems" /> <UNavigationMenu :items="menuItems" class="w-full" />
<!-- {{ router }} --> <!-- {{ router }} -->
<!-- <RouterLink :to="{ name: 'admin-products' }"> <!-- <RouterLink :to="{ name: 'admin-products' }">
@@ -93,8 +90,11 @@ await getTopMenu()
<!-- Theme Switcher --> <!-- Theme Switcher -->
<ThemeSwitch /> <ThemeSwitch />
<!-- Logout Button (only when authenticated) --> <!-- Logout Button (only when authenticated) -->
<button v-if="authStore.isAuthenticated" @click="authStore.logout()" <button
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)"> v-if="authStore.isAuthenticated"
@click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)"
>
{{ $t('general.logout') }} {{ $t('general.logout') }}
</button> </button>
</div> </div>

View File

@@ -10,6 +10,18 @@ function isAuthenticated(): boolean {
} }
await getSettings() await getSettings()
const routes = await getRoutes()
let newRoutes = []
for (let r of routes) {
const component = () => import(/* @vite-ignore */ `..${r.component}`)
newRoutes.push({
path: r.path,
component,
name: r.name,
meta: r.meta ? JSON.parse(r.meta) : {},
})
}
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL), history: createWebHistory(import.meta.env.VITE_BASE_URL),
routes: [ routes: [
@@ -21,8 +33,9 @@ const router = createRouter({
path: '/:locale', path: '/:locale',
name: 'locale', name: 'locale',
children: [ children: [
...newRoutes,
{ {
path: '/:pathMatch(.*)*', path: ':pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'), component: () => import('@/views/NotFoundView.vue'),
name: 'not-found-child', name: 'not-found-child',
}, },
@@ -36,25 +49,6 @@ const router = createRouter({
], ],
}) })
await getRoutes().then(routes => {
const modules = import.meta.glob('/src/**/**/*.vue')
routes.forEach(item => {
const component = modules[`/src${item.Component}`]
if (!component) {
console.error('Component not found:', item.Component)
return
}
router.addRoute('locale', {
path: item.Path,
component,
name: item.Name,
meta: item.Meta ? JSON.parse(item.Meta) : {}
})
})
})
router.beforeEach((to, from) => { router.beforeEach((to, from) => {
const locale = to.params.locale as string const locale = to.params.locale as string
const localeLang = langs.find((x) => x.iso_code === locale) const localeLang = langs.find((x) => x.iso_code === locale)

View File

@@ -10,7 +10,7 @@ export const getMenu = async () => {
export const getRoutes = async () => { export const getRoutes = async () => {
const resp = await useFetchJson<Route[]>('/api/v1/restricted/menu/get-routes'); const resp = await useFetchJson<Route[]>('/api/v1/public/menu/get-routes');
return resp.items return resp.items

View File

@@ -1,8 +1,9 @@
export * from '@types/lang' export * from '@types/lang'
export * from '@types/response' export * from '@types/response'
export * from '@types/menu'
export interface ApiResponse { export interface ApiResponse<T> {
message: string message: string
items: Product[] items: T
count: number count: number
} }

View File

@@ -12,15 +12,42 @@ export interface Params {
} }
export interface Route { export interface Route {
ID: number id: number
Name: string name: string
Path: string path: string
Component: string component: string
Layout: string meta: string
Meta: string active: boolean
IsActive: boolean }
SortOrder: number
ParentID: any export interface TopMenuItem {
Parent: any menu_id: number
Children: any label: Label
} params: TopMenuParams
active: number
position: number
children: TopMenuItem[]
}
export interface Label {
label: string
trans:LabelTrans
}
export interface LabelTrans{
pl:LabelItem
en:LabelItem
de: LabelItem
}
export interface LabelItem {
label: string
}
export interface TopMenuParams {
route: TopMenuRoute
}
export interface TopMenuRoute {
name: string
params: Record<string, string>
}

View File

@@ -0,0 +1,9 @@
<template>
<component :is="Default || 'div'">
home View
</component>
</template>
<script setup lang="ts">
import Default from '@/layouts/default.vue';
</script>

View File

@@ -5,28 +5,21 @@ CREATE TABLE IF NOT EXISTS b2b_routes (
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE,
path VARCHAR(255) NULL, path VARCHAR(255) NULL,
component VARCHAR(255) NOT NULL COMMENT 'path to component file', component VARCHAR(255) NOT NULL COMMENT 'path to component file',
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
meta JSON DEFAULT '{}', meta JSON DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE, active BOOLEAN DEFAULT TRUE
sort_order INT DEFAULT 0,
parent_id INT NULL,
CONSTRAINT fk_parent
FOREIGN KEY (parent_id)
REFERENCES b2b_routes(id)
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO b2b_routes INSERT IGNORE INTO b2b_routes
(name, path, component, layout, meta, is_active, sort_order, parent_id) (name, path, component, meta, active)
VALUES VALUES
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0), ('root', '', '', '{"trans": "route.root"}', 0),
('home', '', '@/views/HomeView.vue', 'default', '{"trans": "route.home"}', 1, 0, 0), ('home', '', '/views/HomeView.vue', '{"trans": "route.home"}', 1),
('login', 'login', '@/views/LoginView.vue', 'empty', '{"guest":true}', 1, 2, NULL), ('login', 'login', '/views/LoginView.vue', '{"guest":true}', 1),
('register', 'register', '@/views/RegisterView.vue', 'empty', '{"guest":true}', 1, 3, NULL), ('register', 'register', '/views/RegisterView.vue', '{"guest":true}', 1),
('password-recovery', 'password-recovery', '@/views/PasswordRecoveryView.vue', 'empty', '{"guest":true}', 1, 4, NULL), ('password-recovery', 'password-recovery', '/views/PasswordRecoveryView.vue', '{"guest":true}', 1),
('reset-password', 'reset-password', '@/views/ResetPasswordView.vue', 'empty', '{"guest":true}', 1, 5, NULL), ('reset-password', 'reset-password', '/views/ResetPasswordView.vue', '{"guest":true}', 1),
('verify-email', 'verify-email', '@/views/VerifyEmailView.vue', 'empty', '{"guest":true}', 1, 6, NULL); ('verify-email', 'verify-email', '/views/VerifyEmailView.vue', '{"guest":true}', 1);
CREATE TABLE IF NOT EXISTS b2b_top_menu ( CREATE TABLE IF NOT EXISTS b2b_top_menu (
@@ -44,10 +37,10 @@ CREATE TABLE IF NOT EXISTS b2b_top_menu (
) ENGINE = InnoDB; ) ENGINE = InnoDB;
INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES INSERT IGNORE INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES
(1, JSON_COMPACT('{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}'),NULL,'{}',1,1), (1, JSON_COMPACT('{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}'),NULL,JSON_COMPACT('{}'),1,1),
(3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,'{}',1,1), (3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,JSON_COMPACT('{}'),1,1),
(9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,'{}',1,1); (9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,JSON_COMPACT('{"route": {"name": "home", "params":{"locale": ""}}}'),1,1);
@@ -55,4 +48,4 @@ INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`,
DROP TABLE IF EXISTS b2b_routes; DROP TABLE IF EXISTS b2b_routes;
DROP TABLE IF EXISTS b2b_top_menu; DROP TABLE IF EXISTS b2b_top_menu;
DROP FUNCTION IF EXISTS `slugify_eu`;

View File

@@ -138,6 +138,68 @@ VALUES
(3, 'Čeština', 2, '🇨🇿'), (3, 'Čeština', 2, '🇨🇿'),
(4, 'Deutschland', 2, '🇩🇪'); (4, 'Deutschland', 2, '🇩🇪');
DELIMITER //
CREATE FUNCTION IF NOT EXISTS slugify_eu(input TEXT)
RETURNS TEXT
DETERMINISTIC
BEGIN
DECLARE s TEXT;
SET s = LOWER(input);
-- spaces
SET s = REPLACE(s,' ','_');
-- Polish
SET s = REPLACE(s,'ą','a');
SET s = REPLACE(s,'ć','c');
SET s = REPLACE(s,'ę','e');
SET s = REPLACE(s,'ł','l');
SET s = REPLACE(s,'ń','n');
SET s = REPLACE(s,'ó','o');
SET s = REPLACE(s,'ś','s');
SET s = REPLACE(s,'ż','z');
SET s = REPLACE(s,'ź','z');
-- German
SET s = REPLACE(s,'ä','a');
SET s = REPLACE(s,'ö','o');
SET s = REPLACE(s,'ü','u');
SET s = REPLACE(s,'ß','ss');
-- French
SET s = REPLACE(s,'à','a');
SET s = REPLACE(s,'â','a');
SET s = REPLACE(s,'æ','ae');
SET s = REPLACE(s,'ç','c');
SET s = REPLACE(s,'è','e');
SET s = REPLACE(s,'é','e');
SET s = REPLACE(s,'ê','e');
SET s = REPLACE(s,'ë','e');
SET s = REPLACE(s,'î','i');
SET s = REPLACE(s,'ï','i');
SET s = REPLACE(s,'ô','o');
SET s = REPLACE(s,'ù','u');
SET s = REPLACE(s,'û','u');
SET s = REPLACE(s,'ü','u');
SET s = REPLACE(s,'ÿ','y');
-- Spanish / Portuguese
SET s = REPLACE(s,'á','a');
SET s = REPLACE(s,'í','i');
SET s = REPLACE(s,'ñ','n');
-- Scandinavian
SET s = REPLACE(s,'å','a');
SET s = REPLACE(s,'ø','o');
RETURN s;
END //
DELIMITER ;
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_countries; DROP TABLE IF EXISTS b2b_countries;

View File

@@ -21,7 +21,7 @@ INSERT IGNORE b2b_scopes (id, name) VALUES (3, 'backoffice');
-- Translations -- Translations
-- Component: general -- Component: general
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
(1, 3, 300, 'already_have_an_account', 'Masz już konto?'), (1, 3, 300, 'already_have_an_account', 'Masz już konto?'),
(1, 3, 300, 'and', 'i'), (1, 3, 300, 'and', 'i'),
(1, 3, 300, 'back_to_sign_in', 'Powrót do logowania'), (1, 3, 300, 'back_to_sign_in', 'Powrót do logowania'),
@@ -123,10 +123,12 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 3, 300, 'reset_password', 'Resetovat heslo'), (3, 3, 300, 'reset_password', 'Resetovat heslo'),
(3, 3, 300, 'send_password_reset_link', 'Odeslat odkaz pro obnovení hesla'), (3, 3, 300, 'send_password_reset_link', 'Odeslat odkaz pro obnovení hesla'),
(3, 3, 300, 'sign_in', 'Přihlásit se'), (3, 3, 300, 'sign_in', 'Přihlásit se'),
(3, 3, 300, 'terms_of_service', 'Podmínky služby'); (3, 3, 300, 'terms_of_service', 'Podmínky služby')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- Component: validate_error -- Component: validate_error
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
(1, 3, 301, 'confirm_password_required', 'Potwierdź hasło'), (1, 3, 301, 'confirm_password_required', 'Potwierdź hasło'),
(1, 3, 301, 'email_required', 'Adres e-mail jest wymagany'), (1, 3, 301, 'email_required', 'Adres e-mail jest wymagany'),
(1, 3, 301, 'first_name_required', 'Imię jest wymagane'), (1, 3, 301, 'first_name_required', 'Imię jest wymagane'),
@@ -150,10 +152,12 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 3, 301, 'no_issues_for_quarter', 'Nebyla nalezena žádná issues pro toto čtvrtletí'), (3, 3, 301, 'no_issues_for_quarter', 'Nebyla nalezena žádná issues pro toto čtvrtletí'),
(3, 3, 301, 'password_required', 'Heslo je povinné'), (3, 3, 301, 'password_required', 'Heslo je povinné'),
(3, 3, 301, 'registration_validation_password_not_same', 'Hesla se neshodují'), (3, 3, 301, 'registration_validation_password_not_same', 'Hesla se neshodují'),
(3, 3, 301, 'registration_validation_password_requirements', 'Požadavky na heslo při registraci'); (3, 3, 301, 'registration_validation_password_requirements', 'Požadavky na heslo při registraci')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- Component: repo_chart -- Component: repo_chart
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
(1, 3, 302, 'all_quarters', 'Wszystkie kwartały'), (1, 3, 302, 'all_quarters', 'Wszystkie kwartały'),
(1, 3, 302, 'created_on', 'Utworzono'), (1, 3, 302, 'created_on', 'Utworzono'),
(1, 3, 302, 'failed_to_load_issues', 'Nie udało się załadować zadań'), (1, 3, 302, 'failed_to_load_issues', 'Nie udało się załadować zadań'),
@@ -231,7 +235,9 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 3, 302, 'user_initials', 'Iniciály uživatele'), (3, 3, 302, 'user_initials', 'Iniciály uživatele'),
(3, 3, 302, 'work_by_quarter', 'Práce dokončená po čtvrtletích (hodiny)'), (3, 3, 302, 'work_by_quarter', 'Práce dokončená po čtvrtletích (hodiny)'),
(3, 3, 302, 'work_done_by_quarter', 'práce provedená za čtvrtletí'), (3, 3, 302, 'work_done_by_quarter', 'práce provedená za čtvrtletí'),
(3, 3, 302, 'year', 'Rok'); (3, 3, 302, 'year', 'Rok')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- Component: verify_email -- Component: verify_email
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
@@ -267,7 +273,9 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 3, 303, 'success_message', 'Váš e-mail byl úspěšně ověřen.'), (3, 3, 303, 'success_message', 'Váš e-mail byl úspěšně ověřen.'),
(3, 3, 303, 'success_title', 'E-mail ověřen!'), (3, 3, 303, 'success_title', 'E-mail ověřen!'),
(3, 3, 303, 'verification_failed', 'Ověření e-mailu selhalo'), (3, 3, 303, 'verification_failed', 'Ověření e-mailu selhalo'),
(3, 3, 303, 'verifying', 'Ověřování vašeho e-mailu...'); (3, 3, 303, 'verifying', 'Ověřování vašeho e-mailu...')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- +goose Down -- +goose Down
-- Remove translations for this scope -- Remove translations for this scope

View File

@@ -44,7 +44,7 @@ INSERT IGNORE b2b_scopes (id, name) VALUES (1, 'Backend');
-- (3, 1, 1, 'translations_not_loaded', 'Übersetzungen konnten nicht geladen werden'); -- (3, 1, 1, 'translations_not_loaded', 'Übersetzungen konnten nicht geladen werden');
-- Component: email (component_id = 100) -- Component: email (component_id = 100)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
(1, 1, 100, 'langs_loaded', 'Języki załadowane'), (1, 1, 100, 'langs_loaded', 'Języki załadowane'),
(1, 1, 100, 'langs_not_loaded', 'Nie udało się załadować języków'), (1, 1, 100, 'langs_not_loaded', 'Nie udało się załadować języków'),
(1, 1, 100, 'message_nok', 'Błąd'), (1, 1, 100, 'message_nok', 'Błąd'),
@@ -64,10 +64,12 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 1, 100, 'message_nok', 'Fehler'), (3, 1, 100, 'message_nok', 'Fehler'),
(3, 1, 100, 'message_ok', 'Erfolg'), (3, 1, 100, 'message_ok', 'Erfolg'),
(3, 1, 100, 'translations_loaded', 'Übersetzungen geladen'), (3, 1, 100, 'translations_loaded', 'Übersetzungen geladen'),
(3, 1, 100, 'translations_not_loaded', 'Übersetzungen konnten nicht geladen werden'); (3, 1, 100, 'translations_not_loaded', 'Übersetzungen konnten nicht geladen werden')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- Component: error (component_id = 101) -- Component: error (component_id = 101)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
(1, 1, 101, 'err_bad_paging', 'zła paginacja'), (1, 1, 101, 'err_bad_paging', 'zła paginacja'),
(1, 1, 101, 'err_bad_quarter_attribute', 'nieprawidłowy atrybut quarter'), (1, 1, 101, 'err_bad_quarter_attribute', 'nieprawidłowy atrybut quarter'),
(1, 1, 101, 'err_bad_repo_id_attribute', 'nieprawidłowy atrybut repoID'), (1, 1, 101, 'err_bad_repo_id_attribute', 'nieprawidłowy atrybut repoID'),
@@ -150,7 +152,9 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 1, 101, 'err_token_required', 'Token erforderlich'), (3, 1, 101, 'err_token_required', 'Token erforderlich'),
(3, 1, 101, 'err_user_inactive', 'Benutzerkonto ist inaktiv'), (3, 1, 101, 'err_user_inactive', 'Benutzerkonto ist inaktiv'),
(3, 1, 101, 'err_user_not_found', 'Benutzer nicht gefunden'), (3, 1, 101, 'err_user_not_found', 'Benutzer nicht gefunden'),
(3, 1, 101, 'err_verification_token_expired', 'Verifizierungstoken abgelaufen'); (3, 1, 101, 'err_verification_token_expired', 'Verifizierungstoken abgelaufen')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- +goose Down -- +goose Down
-- Remove b2b_translations for this scope -- Remove b2b_translations for this scope

View File

@@ -16,7 +16,7 @@ INSERT IGNORE INTO b2b_components (id, name) VALUES (304, 'products');
INSERT IGNORE INTO b2b_components (id, name) VALUES (305, 'nav'); INSERT IGNORE INTO b2b_components (id, name) VALUES (305, 'nav');
-- Component: products (component_id = 304) -- Component: products (component_id = 304)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
-- English (lang_id = 1) -- English (lang_id = 1)
(1, 3, 304, 'title', 'Products'), (1, 3, 304, 'title', 'Products'),
(1, 3, 304, 'image', 'Image'), (1, 3, 304, 'image', 'Image'),
@@ -88,10 +88,12 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(3, 3, 304, 'yes', 'Ano'), (3, 3, 304, 'yes', 'Ano'),
(3, 3, 304, 'no', 'Ne'), (3, 3, 304, 'no', 'Ne'),
(3, 3, 304, 'added_to_cart', 'Přidáno do košíku'), (3, 3, 304, 'added_to_cart', 'Přidáno do košíku'),
(3, 3, 304, 'description', 'Popis'); (3, 3, 304, 'description', 'Popis')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- Component: nav (component_id = 305) -- Component: nav (component_id = 305)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES INSERT b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
-- English (lang_id = 1) -- English (lang_id = 1)
(1, 3, 305, 'chart', 'Chart'), (1, 3, 305, 'chart', 'Chart'),
(1, 3, 305, 'products', 'Products'), (1, 3, 305, 'products', 'Products'),
@@ -100,7 +102,9 @@ INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VA
(2, 3, 305, 'products', 'Produkty'), (2, 3, 305, 'products', 'Produkty'),
-- Czech (lang_id = 3) -- Czech (lang_id = 3)
(3, 3, 305, 'chart', 'Graf'), (3, 3, 305, 'chart', 'Graf'),
(3, 3, 305, 'products', 'Produkty'); (3, 3, 305, 'products', 'Produkty')
ON DUPLICATE KEY UPDATE
data = IF(data IS NULL, VALUES(data), data);
-- +goose Down -- +goose Down
-- Remove translations for these components -- Remove translations for these components