12 Commits

Author SHA1 Message Date
0f494d3abd routing 2026-03-23 16:57:59 +01:00
Daniel Goc
528f12b065 fixed broken HTMLs in ps_product_lang 2026-03-23 12:56:11 +01:00
Daniel Goc
25ad592be3 rename and move folders and files 2026-03-23 09:35:20 +01:00
Daniel Goc
26e6a3c384 setup meili search 2026-03-20 15:18:27 +01:00
a4c1773415 Merge pull request 'fix migrations' (#15) from mailisearch into main
Reviewed-on: #15
2026-03-20 14:02:45 +00:00
8e07daac66 fix migrations 2026-03-20 14:57:50 +01:00
6408b93e5c Merge pull request 'meilisearch' (#14) from mailisearch into main
Reviewed-on: #14
2026-03-20 12:55:52 +00:00
27fa88b076 meilisearch 2026-03-20 13:55:20 +01:00
Daniel Goc
b67c4e3aef endpoint returning tree of categories 2026-03-20 12:38:41 +01:00
Daniel Goc
0d29d8f6a2 debug 2026-03-20 09:57:20 +01:00
Daniel Goc
884e15bb8a added ImageID and LinkRewrite 2026-03-20 09:31:08 +01:00
Daniel Goc
1ea50af96a change returned product list struct 2026-03-19 15:16:08 +01:00
49 changed files with 4609 additions and 682 deletions

4
.env
View File

@@ -21,6 +21,10 @@ AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
AUTH_JWT_EXPIRATION=86400 AUTH_JWT_EXPIRATION=86400
AUTH_REFRESH_EXPIRATION=604800 AUTH_REFRESH_EXPIRATION=604800
# Meili search
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# Google Translate Client # Google Translate Client
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
GOOGLE_CLOUD_PROJECT_ID=translation-343517 GOOGLE_CLOUD_PROJECT_ID=translation-343517

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ bin/
i18n/*.json i18n/*.json
*_templ.go *_templ.go
tmp/main tmp/main
test.go

2995
ADD_THIS_TO_SQL.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -73,10 +73,22 @@ vars:
MP_SMTP_AUTH_ALLOW_INSECURE: true MP_SMTP_AUTH_ALLOW_INSECURE: true
MP_ENABLE_SPAMASSASSIN: postmark MP_ENABLE_SPAMASSASSIN: postmark
MP_VERBOSE: true MP_VERBOSE: true
meilisearch:
image: getmeili/meilisearch:latest
container_name: meilisearch
restart: unless-stopped
ports:
- 7700:7700
volumes:
- meilisearch:/data.ms
environment:
MEILI_MASTER_KEY: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
volumes: volumes:
db_data: db_data:
mailpit_data: mailpit_data:
meilisearch:
includes: includes:

View File

@@ -1,52 +0,0 @@
package restricted
import (
"git.ma-al.com/goc_daniel/b2b/app/service/langsAndCountriesService"
"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"
)
// LangsAndCountriesHandler for getting languages and countries data
type LangsAndCountriesHandler struct {
langsAndCountriesService *langsAndCountriesService.LangsAndCountriesService
}
// NewLangsAndCountriesHandler creates a new LangsAndCountriesHandler instance
func NewLangsAndCountriesHandler() *LangsAndCountriesHandler {
langsAndCountriesService := langsAndCountriesService.New()
return &LangsAndCountriesHandler{
langsAndCountriesService: langsAndCountriesService,
}
}
func LangsAndCountriesHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewLangsAndCountriesHandler()
r.Get("/get-languages", handler.GetLanguages)
r.Get("/get-countries", handler.GetCountries)
return r
}
func (h *LangsAndCountriesHandler) GetLanguages(c fiber.Ctx) error {
languages, err := h.langsAndCountriesService.GetLanguages()
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
}
func (h *LangsAndCountriesHandler) GetCountries(c fiber.Ctx) error {
countries, err := h.langsAndCountriesService.GetCountriesAndCurrencies()
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -1,6 +1,8 @@
package restricted package restricted
import ( import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService" "git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -50,7 +52,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
// "override_currency": c.Query("override_currency", ""), // "override_currency": c.Query("override_currency", ""),
// } // }
listing, err := h.listProductsService.GetListing(paging, filters) id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
listing, err := h.listProductsService.GetListing(uint(id_lang), paging, filters)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -0,0 +1,52 @@
package restricted
import (
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
"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"
)
// LocaleSelectorHandler for getting languages and countries data
type LocaleSelectorHandler struct {
localeSelectorService *localeSelectorService.LocaleSelectorService
}
// NewLocaleSelectorHandler creates a new LocaleSelectorHandler instance
func NewLocaleSelectorHandler() *LocaleSelectorHandler {
localeSelectorService := localeSelectorService.New()
return &LocaleSelectorHandler{
localeSelectorService: localeSelectorService,
}
}
func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewLocaleSelectorHandler()
r.Get("/get-languages", handler.GetLanguages)
r.Get("/get-countries", handler.GetCountries)
return r
}
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
languages, err := h.localeSelectorService.GetLanguages()
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
}
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -0,0 +1,65 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
"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 MeiliSearchHandler struct {
meiliService *meiliService.MeiliService
}
func NewMeiliSearchHandler() *MeiliSearchHandler {
meiliService := meiliService.New()
return &MeiliSearchHandler{
meiliService: meiliService,
}
}
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMeiliSearchHandler()
r.Get("/test", handler.Test)
r.Get("/create-index", handler.CreateIndex)
return r
}
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.meiliService.CreateIndex(uint(id_lang))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
nothing := ""
return c.JSON(response.Make(&nothing, 0, i18n.T_(c, response.Message_OK)))
}
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
test, err := h.meiliService.Test(uint(id_lang))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -0,0 +1,60 @@
package restricted
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 MenuHandler struct {
menuService *menuService.MenuService
}
func NewMenuHandler() *MenuHandler {
menuService := menuService.New()
return &MenuHandler{
menuService: menuService,
}
}
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler()
r.Get("/get-menu", handler.GetMenu)
r.Get("/get-routes", handler.GetRouting)
return r
}
func (h *MenuHandler) GetMenu(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.GetMenu(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) 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

@@ -54,13 +54,6 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
productShopID_attribute := c.Query("productShopID")
productShopID, err := strconv.Atoi(productShopID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
productLangID_attribute := c.Query("productLangID") productLangID_attribute := c.Query("productLangID")
productLangID, err := strconv.Atoi(productLangID_attribute) productLangID, err := strconv.Atoi(productLangID_attribute)
if err != nil { if err != nil {
@@ -68,7 +61,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID)) description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productLangID))
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -77,7 +70,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
} }
// SaveProductDescription saves the description for a given product ID, in given shop and language // SaveProductDescription saves the description for a given product ID, in given language
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error { func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
@@ -92,13 +85,6 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
productShopID_attribute := c.Query("productShopID")
productShopID, err := strconv.Atoi(productShopID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
productLangID_attribute := c.Query("productLangID") productLangID_attribute := c.Query("productLangID")
productLangID, err := strconv.Atoi(productLangID_attribute) productLangID, err := strconv.Atoi(productLangID_attribute)
if err != nil { if err != nil {
@@ -112,7 +98,7 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates) err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -136,13 +122,6 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
productShopID_attribute := c.Query("productShopID")
productShopID, err := strconv.Atoi(productShopID_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
productFromLangID_attribute := c.Query("productFromLangID") productFromLangID_attribute := c.Query("productFromLangID")
productFromLangID, err := strconv.Atoi(productFromLangID_attribute) productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
if err != nil { if err != nil {
@@ -163,7 +142,7 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel) description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -97,10 +97,22 @@ func (s *Server) Setup() error {
listProducts := s.restricted.Group("/list-products") listProducts := s.restricted.Group("/list-products")
restricted.ListProductsHandlerRoutes(listProducts) restricted.ListProductsHandlerRoutes(listProducts)
// changing the JWT cookies routes (restricted) // locale selector (restricted)
// in reality it just handles changing user's country and language // this is basically for changing user's selected language and country
langsAndCountries := s.restricted.Group("/langs-and-countries") localeSelector := s.restricted.Group("/langs-and-countries")
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries) restricted.LocaleSelectorHandlerRoutes(localeSelector)
// menu (restricted)
menu := s.restricted.Group("/menu")
restricted.MenuHandlerRoutes(menu)
// meili search (restricted)
meiliSearch := s.restricted.Group("/meili-search")
restricted.MeiliSearchHandlerRoutes(meiliSearch)
s.api.All("*", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
})
// // Restricted routes example // // Restricted routes example
// restricted := s.api.Group("/restricted") // restricted := s.api.Group("/restricted")

View File

@@ -61,6 +61,13 @@ type Product struct {
State uint `gorm:"column:state" json:"state" form:"state"` State uint `gorm:"column:state" json:"state" form:"state"`
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"` DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
} }
type ProductInList struct {
ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
Name string `gorm:"column:name" json:"name" form:"name"`
ImageID uint `gorm:"column:id_image"`
LinkRewrite string `gorm:"column:link_rewrite"`
Active uint `gorm:"column:active" json:"active" form:"active"`
}
type ProductFilters struct { type ProductFilters struct {
Sort string `json:"sort,omitempty" query:"sort,omitempty" example:"price,asc;name,desc"` // sort rule Sort string `json:"sort,omitempty" query:"sort,omitempty" example:"price,asc;name,desc"` // sort rule
@@ -74,4 +81,28 @@ type ProductFilters struct {
InStock uint `query:"stock,omitempty"` InStock uint `query:"stock,omitempty"`
} }
type ScannedCategory struct {
CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategpryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategpryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
}
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint

View File

@@ -18,3 +18,11 @@ type ProductDescription struct {
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"` DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"` Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
} }
type MeiliSearchProduct struct {
ProductID uint
Name string
Description string
DescriptionShort string
Usage string
}

21
app/model/routing.go Normal file
View File

@@ -0,0 +1,21 @@
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"`
}
func (Route) TableName() string {
return "b2b_routes"
}

View File

@@ -0,0 +1,41 @@
package categoriesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UICategoriesRepo interface {
GetAllCategories(id_lang uint) ([]model.ScannedCategory, error)
}
type CategoriesRepo struct{}
func New() UICategoriesRepo {
return &CategoriesRepo{}
}
func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
err := db.DB.Raw(`
SELECT
ps_category.id_category AS ID,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
FROM ps_category
LEFT JOIN ps_category_lang ON ps_category_lang.id_category = ps_category.id_category AND ps_category_lang.id_shop = ? AND ps_category_lang.id_lang = ?
LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ?
JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang
`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -0,0 +1,82 @@
package listProductsRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UIListProductsRepo interface {
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
}
type ListProductsRepo struct{}
func New() UIListProductsRepo {
return &ListProductsRepo{}
}
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
var listing []model.ProductInList
var total int64
// var resultIDs []uint
// q := db.DB.
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
// // MySQL. It works when followed by `SELECT FOUND_ROWS();`. To learn
// // more see: https://mariarawmodel.com/kb/en/found_rows/
// // WARN: This might not work on different SQL databases
// Select("DISTINCT SQL_CALC_FOUND_ROWS id").
// // Debug().
// Scopes(view.FromDBViewForDisplay(langID, countryIso)).
// Scopes(scopesForFiltersOnDisplay(db.DB, langID, countryIso, filt)).
// Scopes(filt.OfCategory(filters.ORDER_FILTER)...).
// Limit(p.Limit()).
// Offset(p.Offset())
err := db.DB.Raw(`
SELECT
ps_product.id_product AS ID,
ps_product_lang.name AS name,
ps_product.active AS active,
ps_product_lang.link_rewrite AS link_rewrite,
COALESCE (
ps_image_shop.id_image, any_image.id_image
) AS id_image
FROM ps_product
LEFT JOIN ps_product_lang
ON ps_product_lang.id_product = ps_product.id_product
AND ps_product_lang.id_shop = ?
AND ps_product_lang.id_lang = ?
LEFT JOIN ps_image_shop
ON ps_image_shop.id_product = ps_product.id_product
AND ps_image_shop.id_shop = ?
AND ps_image_shop.cover = 1
LEFT JOIN (
SELECT id_product, MIN(id_image) AS id_image
FROM ps_image
GROUP BY id_product
) any_image
ON ps_product.id_product = any_image.id_product
LIMIT ? OFFSET ?`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID, p.Limit(), p.Offset()).
Scan(&listing).Error
if err != nil {
return find.Found[model.ProductInList]{}, err
}
err = db.DB.Raw(`
SELECT COUNT(*)
FROM ps_product`).
Scan(&total).Error
if err != nil {
return find.Found[model.ProductInList]{}, err
}
return find.Found[model.ProductInList]{
Items: listing,
Count: uint(total),
}, nil
}

View File

@@ -1,22 +1,22 @@
package langsAndCountriesRepo package localeSelectorRepo
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"
) )
type UILangsAndCountriesRepo interface { type UILocaleSelectorRepo interface {
GetLanguages() ([]model.Language, error) GetLanguages() ([]model.Language, error)
GetCountriesAndCurrencies() ([]model.Country, error) GetCountriesAndCurrencies() ([]model.Country, error)
} }
type LangsAndCountriesRepo struct{} type LocaleSelectorRepo struct{}
func New() UILangsAndCountriesRepo { func New() UILocaleSelectorRepo {
return &LangsAndCountriesRepo{} return &LocaleSelectorRepo{}
} }
func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) { func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
var languages []model.Language var languages []model.Language
err := db.DB.Table("b2b_language").Scan(&languages).Error err := db.DB.Table("b2b_language").Scan(&languages).Error
@@ -24,7 +24,7 @@ func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
return languages, err return languages, err
} }
func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) { func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
var countries []model.Country var countries []model.Country
err := db.DB.Table("b2b_countries"). err := db.DB.Table("b2b_countries").

View File

@@ -5,12 +5,13 @@ 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"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
) )
type UIProductDescriptionRepo interface { type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error CreateIfDoesNotExist(productID uint, productLangID uint) error
UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error UpdateFields(productID uint, productLangID uint, updates map[string]string) error
} }
type ProductDescriptionRepo struct{} type ProductDescriptionRepo struct{}
@@ -20,12 +21,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, productShopID uint, productLangID uint) (*model.ProductDescription, error) { func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID 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, productShopID, productLangID). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
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)
@@ -35,16 +36,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productSh
} }
// If it doesn't exist, returns an error. // If it doesn't exist, returns an error.
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error { func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
record := model.ProductDescription{ record := model.ProductDescription{
ProductID: productID, ProductID: productID,
ShopID: productShopID, ShopID: constdata.SHOP_ID,
LangID: productLangID, LangID: productLangID,
} }
err := db.DB. err := db.DB.
Table("ps_product_lang"). Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
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)
@@ -53,7 +54,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productSho
return nil return nil
} }
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error { func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
if len(updates) == 0 { if len(updates) == 0 {
return nil return nil
} }
@@ -64,7 +65,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID 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, productShopID, productLangID). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
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)

View File

@@ -0,0 +1,25 @@
package routesrepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UIRoutesRepo interface {
GetRoutes(langId uint) ([]model.Route, error)
}
type RoutesRepo struct{}
func New() UIRoutesRepo {
return &RoutesRepo{}
}
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
routes := []model.Route{}
err := db.DB.Find(&routes).Error
if err != nil {
return nil, err
}
return routes, nil
}

View File

@@ -1,26 +0,0 @@
package langsAndCountriesService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/repository/langsAndCountriesRepo"
)
// LangsAndCountriesService literally sends back language and countries information.
type LangsAndCountriesService struct {
repo langsAndCountriesRepo.UILangsAndCountriesRepo
}
// NewLangsAndCountriesService creates a new LangsAndCountries service
func New() *LangsAndCountriesService {
return &LangsAndCountriesService{
repo: langsAndCountriesRepo.New(),
}
}
func (s *LangsAndCountriesService) GetLanguages() ([]model.Language, error) {
return s.repo.GetLanguages()
}
func (s *LangsAndCountriesService) GetCountriesAndCurrencies() ([]model.Country, error) {
return s.repo.GetCountriesAndCurrencies()
}

View File

@@ -1,7 +1,7 @@
package langsService package langsService
import ( import (
langs_repo "git.ma-al.com/goc_daniel/b2b/app/langs" langs_repo "git.ma-al.com/goc_daniel/b2b/app/repos/langsRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "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/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"

View File

@@ -2,9 +2,9 @@ package listProductsService
import ( import (
"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/repos/listProductsRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/repository/listProductsRepo"
) )
type ListProductsService struct { type ListProductsService struct {
@@ -17,8 +17,8 @@ func New() *ListProductsService {
} }
} }
func (s *ListProductsService) GetListing(p find.Paging, filters *filters.FiltersList) (find.Found[model.Product], error) { func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
var products find.Found[model.Product] var products find.Found[model.ProductInList]
// currencyIso := c.Cookies("currency_iso", "") // currencyIso := c.Cookies("currency_iso", "")
// countryIso := c.Cookies("country_iso", "") // countryIso := c.Cookies("country_iso", "")
@@ -30,7 +30,7 @@ func (s *ListProductsService) GetListing(p find.Paging, filters *filters.Filters
// countryIso = overrides["override_country"] // countryIso = overrides["override_country"]
// } // }
products, err := s.listProductsRepo.GetListing(p, filters) products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
if err != nil { if err != nil {
return products, err return products, err
} }

View File

@@ -0,0 +1,26 @@
package localeSelectorService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/localeSelectorRepo"
)
// LocaleSelectorService literally sends back language and countries information.
type LocaleSelectorService struct {
repo localeSelectorRepo.UILocaleSelectorRepo
}
// NewLocaleSelectorService creates a new LocaleSelector service
func New() *LocaleSelectorService {
return &LocaleSelectorService{
repo: localeSelectorRepo.New(),
}
}
func (s *LocaleSelectorService) GetLanguages() ([]model.Language, error) {
return s.repo.GetLanguages()
}
func (s *LocaleSelectorService) GetCountriesAndCurrencies() ([]model.Country, error) {
return s.repo.GetCountriesAndCurrencies()
}

View File

@@ -0,0 +1,185 @@
package meiliService
import (
"encoding/xml"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go"
)
type MeiliService struct {
meiliClient meilisearch.ServiceManager
}
func New() *MeiliService {
meiliURL := os.Getenv("MEILISEARCH_URL")
meiliAPIKey := os.Getenv("MEILISEARCH_API_KEY")
client := meilisearch.New(
meiliURL,
meilisearch.WithAPIKey(meiliAPIKey),
)
return &MeiliService{
meiliClient: client,
}
}
// ==================================== FOR SUPERADMIN ONLY ====================================
func (s *MeiliService) CreateIndex(id_lang uint) error {
var products []model.ProductDescription
err := db.DB.
Table("ps_product_lang").
Where("id_shop = ? AND id_lang = ?", constdata.SHOP_ID, id_lang).
Scan(&products).Error
if err != nil {
return fmt.Errorf("database error: %w", err)
}
var meiliProducts []model.MeiliSearchProduct
for i := 0; i < len(products); i++ {
var nextMeiliProduct model.MeiliSearchProduct
nextMeiliProduct.ProductID = products[i].ProductID
nextMeiliProduct.Name = products[i].Name
nextMeiliProduct.Description, err = cleanHTML(products[i].Description)
if err != nil {
fmt.Printf("nextMeiliProduct.Description: %v\n", nextMeiliProduct.Description)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description")
fmt.Printf("err: %v\n", err)
return err
}
nextMeiliProduct.DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
if err != nil {
fmt.Printf("nextMeiliProduct.DescriptionShort: %v\n", nextMeiliProduct.DescriptionShort)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description short")
fmt.Printf("err: %v\n", err)
return err
}
nextMeiliProduct.Usage, err = cleanHTML(products[i].Usage)
if err != nil {
fmt.Printf("nextMeiliProduct.Usage: %v\n", nextMeiliProduct.Usage)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at usage")
fmt.Printf("err: %v\n", err)
return err
}
meiliProducts = append(meiliProducts, nextMeiliProduct)
}
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
primaryKey := "ProductID"
docOptions := &meilisearch.DocumentOptions{
PrimaryKey: &primaryKey,
SkipCreation: false,
}
task, err := s.meiliClient.Index(indexName).AddDocuments(meiliProducts, docOptions)
if err != nil {
return fmt.Errorf("meili AddDocuments error: %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
}
// ==================================== FOR DEBUG 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)
searchReq := &meilisearch.SearchRequest{
Limit: 3,
}
// Perform search
results, err := s.meiliClient.Index(indexName).Search("walek", searchReq)
if err != nil {
fmt.Printf("Meilisearch error: %v\n", err)
return meilisearch.SearchResponse{}, err
}
fmt.Printf("Search results for query 'walek' in %s: %d hits\n", indexName, len(results.Hits))
return *results, nil
}
// Search performs a full-text search on the specified index
func (s *MeiliService) Search(indexName string, query string, limit int) (meilisearch.SearchResponse, error) {
searchReq := &meilisearch.SearchRequest{
Limit: int64(limit),
}
results, err := s.meiliClient.Index(indexName).Search(query, searchReq)
if err != nil {
fmt.Printf("Meilisearch search error: %v\n", err)
return meilisearch.SearchResponse{}, err
}
return *results, nil
}
// HealthCheck checks if Meilisearch is healthy and accessible
func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
health, err := s.meiliClient.Health()
if err != nil {
return nil, fmt.Errorf("meilisearch health check failed: %w", err)
}
return health, nil
}
// remove all tags from HTML text
func cleanHTML(s string) (string, error) {
r := strings.NewReader(s)
d := xml.NewDecoder(r)
text := ""
// Configure the decoder for HTML; leave off strict and autoclose for XHTML
d.Strict = true
d.AutoClose = xml.HTMLAutoClose
d.Entity = xml.HTMLEntity
for {
token, err := d.Token()
if err == io.EOF {
break
} else if err != nil {
return text, err
}
switch v := token.(type) {
case xml.StartElement:
if len(text) > 0 && text[len(text)-1] != '\n' {
text += " \n "
}
case xml.EndElement:
case xml.CharData:
if strings.TrimSpace(string(v)) != "" {
text += string(v)
}
case xml.Comment:
case xml.ProcInst:
case xml.Directive:
}
}
return text, nil
}

View File

@@ -0,0 +1,99 @@
package menuService
import (
"sort"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo
routesRepo routesRepo.UIRoutesRepo
}
func New() *MenuService {
return &MenuService{
categoriesRepo: categoriesRepo.New(),
routesRepo: routesRepo.New(),
}
}
func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
if err != nil {
return &model.Category{}, err
}
// find the root
root_index := 0
root_found := false
for i := 0; i < len(all_categories); i++ {
if all_categories[i].IsRoot == 1 {
root_index = i
root_found = true
break
}
}
if !root_found {
return &model.Category{}, responseErrors.ErrNoRootFound
}
// now create the children and reorder them according to position
id_to_index := make(map[uint]int)
for i := 0; i < len(all_categories); i++ {
id_to_index[all_categories[i].CategoryID] = i
}
children_indices := make(map[int][]ChildWithPosition)
for i := 0; i < len(all_categories); i++ {
parent_index := id_to_index[all_categories[i].ParentID]
children_indices[parent_index] = append(children_indices[parent_index], ChildWithPosition{Index: i, Position: all_categories[i].Position})
}
for key := range children_indices {
sort.Sort(ByPosition(children_indices[key]))
}
// finally, create the tree
tree := s.createTree(root_index, &all_categories, &children_indices)
return &tree, nil
}
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
node := s.scannedToNormalCategory((*all_categories)[index])
for i := 0; i < len((*children_indices)[index]); i++ {
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
}
return node
}
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
return s.routesRepo.GetRoutes(id_lang)
}
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
var normal model.Category
// normal.Active = scanned.Active
normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name
// normal.Active = scanned.Active == 1
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
normal.Children = []model.Category{}
return normal
}
type ChildWithPosition struct {
Index int
Position uint
}
type ByPosition []ChildWithPosition
func (a ByPosition) Len() int { return len(a) }
func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }

View File

@@ -17,9 +17,9 @@ import (
"cloud.google.com/go/translate/apiv3/translatepb" "cloud.google.com/go/translate/apiv3/translatepb"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"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/repos/productDescriptionRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/service/langsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
"github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses" "github.com/openai/openai-go/v3/responses"
@@ -82,12 +82,12 @@ func New() *ProductDescriptionService {
} }
} }
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) {
return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID) return s.productDescriptionRepo.GetProductDescription(productID, productLangID)
} }
// Updates relevant fields with the "updates" map // Updates relevant fields with the "updates" map
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error { func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error {
// only some fields can be affected // only some fields can be affected
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"} allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
for key := range updates { for key := range updates {
@@ -106,12 +106,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
} }
} }
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID) err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productLangID)
if err != nil { if err != nil {
return err return err
} }
return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates) return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates)
} }
// TranslateProductDescription fetches the product description for productFromLangID, // TranslateProductDescription fetches the product description for productFromLangID,
@@ -120,9 +120,9 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
// //
// The Google Cloud project must have the Cloud Translation API enabled and the // The Google Cloud project must have the Cloud Translation API enabled and the
// service account must hold the "Cloud Translation API User" role. // service account must hold the "Cloud Translation API User" role.
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) { func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID) productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productFromLangID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,3 +2,4 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
const SHOP_ID = 1

View File

@@ -48,6 +48,9 @@ var (
// Typed errors for product list handler // Typed errors for product list handler
ErrBadPaging = errors.New("bad or missing paging attribute value in header") ErrBadPaging = errors.New("bad or missing paging attribute value in header")
// Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table")
) )
// Error represents an error with HTTP status code // Error represents an error with HTTP status code
@@ -135,6 +138,9 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadPaging): case errors.Is(err, ErrBadPaging):
return i18n.T_(c, "error.err_bad_paging") return i18n.T_(c, "error.err_bad_paging")
case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.no_root_found")
default: default:
return i18n.T_(c, "error.err_internal_server_error") return i18n.T_(c, "error.err_internal_server_error")
} }
@@ -169,7 +175,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrBadAttribute), errors.Is(err, ErrBadAttribute),
errors.Is(err, ErrBadField), errors.Is(err, ErrBadField),
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging): errors.Is(err, ErrBadPaging),
errors.Is(err, ErrNoRootFound):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict

1
bo/components.d.ts vendored
View File

@@ -41,6 +41,7 @@ declare module 'vue' {
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default'] UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']

View File

@@ -1,14 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import TopBar from '@/components/TopBar.vue'; import TopBar from '@/components/TopBar.vue';
import { getMenu } from '@/router/menu'
import type { NavigationMenuItem } from '@nuxt/ui';
import { ref } from 'vue';
let menu = await getMenu() as NavigationMenuItem[]
const openAll = ref(false)
function adaptMenu(menu: NavigationMenuItem[]) {
for (const item of menu) {
if(item.children && item.children.length > 0){
console.log(item);
adaptMenu(item.children);
item.open = openAll.value
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
} else {
item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([
[
...menu as NavigationMenuItem[]
],
])
</script> </script>
<template> <template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]"> <div class="h-screen grid grid-rows-[auto_1fr_auto]">
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
<main class="p-10"> <main class="p-10">
<TopBar/> <TopBar/>
<router-view /> <div class="mt-24 w-1/4 bg-accented rounded-2xl">
<button @click="openAll = !openAll">open all</button>
<UNavigationMenu arrow orientation="vertical" :items="items" class="p-4" :key="openAll">
</UNavigationMenu>
</div>
<slot></slot>
</main> </main>
</div> </div>
</template> </template>

View File

@@ -1,7 +1,7 @@
<template> <template>
<main :key="i18n.locale.value"> <main :key="i18n.locale.value">
<TopBarLogin /> <TopBarLogin />
<router-view /> <slot></slot>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,9 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Empty from '@/layouts/empty.vue'
import { currentLang, langs } from './langs' import { currentLang, langs } from './langs'
import { getSettings } from './settings' import { getSettings } from './settings'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { getMenu } from './menu'
function isAuthenticated(): boolean { function isAuthenticated(): boolean {
if (typeof document === 'undefined') return false if (typeof document === 'undefined') return false
@@ -12,6 +12,7 @@ function isAuthenticated(): boolean {
await getSettings() await getSettings()
const routes = await getMenu()
const router = createRouter({ const router = createRouter({
@@ -24,6 +25,7 @@ const router = createRouter({
{ {
path: '/:locale', path: '/:locale',
children: [ children: [
{ path: 'category/:category_id-:link_rewrite', component: () => import('../views/CategoryView.vue'), name: 'category' },
{ {
path: '', path: '',
component: Default, component: Default,
@@ -36,49 +38,88 @@ const router = createRouter({
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' }, { path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
], ],
}, },
{ { path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true, } },
path: '',
component: Empty,
children: [
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true } },
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } }, { path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } }, { path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } }, { path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } }, { path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
], // {
// path: '',
// component: Empty,
// children: [
// ],
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
}, },
], ],
}, },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
},
], ],
}) })
router.beforeEach((to, from, next) => { // router.beforeEach((to, from, next) => {
// const locale = to.params.locale as string
// const localeLang = langs.find((x) => x.iso_code == locale)
// if (locale && langs.length > 0) {
// const authStore = useAuthStore()
// // console.log(authStore.isAuthenticated, to, from)
// // if()
// const validLocale = langs.find((l) => l.lang_code === locale)
// if (validLocale) {
// currentLang.value = localeLang
// if (!to.meta?.guest && !isAuthenticated()) {
// return next({ name: 'login', params: { locale } })
// }
// // return next()
// return next()
// } else if (locale) {
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
// }
// }
// if (!locale && to.path !== '/') {
// return next(`/${currentLang.value?.iso_code}${to.path}`)
// }
// next()
// })
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)
if (locale && langs.length > 0) { if (locale && langs.length > 0) {
const authStore = useAuthStore() const authStore = useAuthStore()
console.log(authStore.isAuthenticated, to, from)
// if()
const validLocale = langs.find((l) => l.lang_code === locale) const validLocale = langs.find((l) => l.lang_code === locale)
if (validLocale) { if (validLocale) {
currentLang.value = localeLang currentLang.value = localeLang
if (!to.meta?.guest && !isAuthenticated()) {
return next({ name: 'login', params: { locale } }) if (!to.meta?.guest && !authStore.isAuthenticated) {
return { name: 'login', params: { locale } }
} }
return next() return true
} else if (locale) { } else if (locale) {
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`) return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
} }
} }
if (!locale && to.path !== '/') { if (!locale && to.path !== '/') {
return next(`/${currentLang.value?.iso_code}${to.path}`) return `/${currentLang.value?.iso_code}${to.path}`
} }
next() return true
}) })
export default router export default router

17
bo/src/router/menu.ts Normal file
View File

@@ -0,0 +1,17 @@
import { useFetchJson } from "@/composable/useFetchJson";
import type { MenuItem, Route } from "@/types/menu";
export const getMenu = async () => {
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu');
return resp.items.children
}
export const getRoutes = async () => {
const resp = await useFetchJson<Route[]>('/api/v1/restricted/menu/get-routes');
return resp.items
}

26
bo/src/types/menu.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
export interface MenuItem {
category_id: number
label: string
params: Params
children: MenuItem[]
}
export interface Params {
category_id?: number
link_rewrite?: string
locale?: string
}
export interface Route {
ID: number
Name: string
Path: string
Component: string
Layout: string
Meta: string
IsActive: boolean
SortOrder: number
ParentID: any
Parent: any
Children: any
}

View File

@@ -0,0 +1,21 @@
<template>
<component :is="Default || 'div'">
<div class="container mt-24">
<div class="row">
<div class="col-12">
<h1>Category</h1>
{{ $route.params }}
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
// import { useRoute } from 'vue-router';
import Default from '@/layouts/default.vue';
// const route = useRoute()
// console.log(route);
</script>

View File

@@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation' import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -52,9 +52,11 @@ const PrivacyComponent = computed(() =>
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')), import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
), ),
) )
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false"> <UDrawer v-model:open="showTherms" :overlay="false">
<template #body> <template #body>
<component :is="TermsComponent" /> <component :is="TermsComponent" />
@@ -163,4 +165,5 @@ const PrivacyComponent = computed(() =>
</p> </p>
</div> </div>
</div> </div>
</component>
</template> </template>

View File

@@ -0,0 +1,7 @@
<template>
<div class="flex flex-col items-center justify-center h-screen">
<h1 class="text-4xl font-bold text-gray-800">404</h1>
<p class="mt-4 text-lg text-gray-600">Page not found</p>
<router-link to="/" class="mt-6 text-blue-500 hover:underline">Go back home</router-link>
</div>
</template>

View File

@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation' import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -36,6 +37,7 @@ function validate(): FormError[] {
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8"> <div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15"> <div class="text-center mb-15">
<div <div
@@ -84,11 +86,13 @@ function validate(): FormError[] {
</UButton> </UButton>
</UForm> </UForm>
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4"> <div
class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="outline" :loading="authStore.loading" <button color="neutral" variant="outline" :loading="authStore.loading"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer" class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin"> @click="goToLogin">
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" /> <UIcon name="mingcute:arrow-left-line"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
{{ $t('general.back_to_sign_in') }} {{ $t('general.back_to_sign_in') }}
</button> </button>
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
@@ -102,4 +106,5 @@ function validate(): FormError[] {
</template> </template>
</div> </div>
</div> </div>
</component>
</template> </template>

View File

@@ -1,4 +1,5 @@
<template> <template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false"> <UDrawer v-model:open="showTherms" :overlay="false">
<template #body> <template #body>
<component :is="TermsComponent" /> <component :is="TermsComponent" />
@@ -110,6 +111,7 @@
</UForm> </UForm>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -120,6 +122,8 @@ import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { locale } = useI18n() const { locale } = useI18n()
const router = useRouter() const router = useRouter()

View File

@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation' import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -49,6 +50,7 @@ function validate(): FormError[] {
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8"> <div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="text-center mb-15"> <div class="text-center mb-15">
<div <div
@@ -85,13 +87,14 @@ function validate(): FormError[] {
class="w-full dark:text-white text-black"> class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'" <UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading" :placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }"> class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
:ui="{ trailing: 'pe-1' }">
<template #trailing> <template #trailing>
<UIcon color="neutral" variant="link" size="sm" <UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'" :name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'" :aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password" :aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" class="mr-2"/> @click="showNewPassword = !showNewPassword" class="mr-2" />
</template> </template>
</UInput> </UInput>
</UFormField> </UFormField>
@@ -100,11 +103,12 @@ function validate(): FormError[] {
class="w-full dark:text-white text-black"> class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'" <UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading" :placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }"> class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
:ui="{ trailing: 'pe-1' }">
<template #trailing> <template #trailing>
<UIcon color="neutral" variant="ghost" size="sm" <UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'" :name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/> @click="showConfirmPassword = !showConfirmPassword" class="mr-2" />
</template> </template>
</UInput> </UInput>
</UFormField> </UFormField>
@@ -125,4 +129,5 @@ function validate(): FormError[] {
</template> </template>
</div> </div>
</div> </div>
</component>
</template> </template>

View File

@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { t, te } = useI18n() const { t, te } = useI18n()
const router = useRouter() const router = useRouter()
@@ -66,6 +67,7 @@ function goToLogin() {
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<div <div
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900"> class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8"> <div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
@@ -133,7 +135,8 @@ function goToLogin() {
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }} {{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin" <button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in') class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
$t('general.sign_in')
}} }}
</button> </button>
</p> </p>
@@ -143,4 +146,5 @@ function goToLogin() {
</div> </div>
</div> </div>
</div> </div>
</component>
</template> </template>

1
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/fiber/v3 v3.1.0
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/meilisearch/meilisearch-go v0.36.1
github.com/openai/openai-go/v3 v3.28.0 github.com/openai/openai-go/v3 v3.28.0
github.com/samber/lo v1.53.0 github.com/samber/lo v1.53.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.48.0

2
go.sum
View File

@@ -109,6 +109,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/meilisearch/meilisearch-go v0.36.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M=
github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=

View File

@@ -1,23 +1,23 @@
-- +goose Up -- +goose Up
-- create routes table -- create routes table
CREATE TABLE IF NOT EXISTS b2b_tracker_routes ( CREATE TABLE IF NOT EXISTS b2b_routes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
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'", layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
meta JSON DEFAULT '{}' , meta JSON DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE, is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0, sort_order INT DEFAULT 0,
parent_id INT NULL 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;
ALTER TABLE b2b_tracker_routes INSERT IGNORE INTO b2b_routes
ADD CONSTRAINT fk_parent
FOREIGN KEY (parent_id) REFERENCES b2b_tracker_routes(id)
ON DELETE SET NULL;
INSERT IGNORE INTO b2b_tracker_routes
(name, path, component, layout, meta, is_active, sort_order, parent_id) (name, path, component, layout, meta, is_active, sort_order, parent_id)
VALUES VALUES
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0), ('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
@@ -30,5 +30,5 @@ VALUES
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_tracker_routes; DROP TABLE IF EXISTS b2b_routes;

View File

@@ -113,13 +113,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_to
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
-- insert sample admin user admin@ma-al.com/Maal12345678
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
VALUES
(1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 'admin', 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 1, 1, '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL);
ALTER TABLE b2b_customers AUTO_INCREMENT = 1;
-- countries -- countries
CREATE TABLE IF NOT EXISTS b2b_countries ( CREATE TABLE IF NOT EXISTS b2b_countries (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,

View File

@@ -1,224 +0,0 @@
-- +goose Up
DELIMITER //
DROP PROCEDURE IF EXISTS get_full_product //
CREATE PROCEDURE get_full_product(
IN p_id_product INT UNSIGNED,
IN p_id_shop INT UNSIGNED,
IN p_id_lang INT UNSIGNED,
IN p_id_customer INT UNSIGNED,
IN p_id_group INT UNSIGNED,
IN p_id_currency INT UNSIGNED,
IN p_id_country INT UNSIGNED,
IN p_quantity INT UNSIGNED
)
BEGIN
DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0;
SELECT COALESCE(t.rate, 0.0000) INTO v_tax_rate
FROM ps_tax_rule tr
INNER JOIN ps_tax t
ON t.id_tax = tr.id_tax
WHERE tr.id_tax_rules_group = (
SELECT ps.id_tax_rules_group
FROM ps_product_shop ps
WHERE ps.id_product = p_id_product
AND ps.id_shop = p_id_shop
LIMIT 1
)
AND tr.id_country = p_id_country
ORDER BY
tr.id_state DESC,
tr.zipcode_from != '' DESC,
tr.id_tax_rule DESC
LIMIT 1;
/* FINAL JSON */
SELECT JSON_OBJECT(
/* ================= PRODUCT ================= */
'id_product', p.id_product,
'reference', p.reference,
'name', pl.name,
'description', pl.description,
'short_description', pl.description_short,
/* ================= PRICE ================= */
'price', JSON_OBJECT(
'base', COALESCE(ps.price, p.price),
'final_tax_excl',
(
COALESCE(ps.price, p.price)
- IFNULL(
CASE
WHEN sp.reduction_type = 'amount' THEN sp.reduction
WHEN sp.reduction_type = 'percentage' THEN COALESCE(ps.price, p.price) * sp.reduction
ELSE 0
END, 0
)
),
'final_tax_incl',
(
(
COALESCE(ps.price, p.price)
- IFNULL(
CASE
WHEN sp.reduction_type = 'amount' THEN sp.reduction
WHEN sp.reduction_type = 'percentage' THEN COALESCE(ps.price, p.price) * sp.reduction
ELSE 0
END, 0
)
) * (1 + v_tax_rate / 100)
)
),
/* ================= META ================= */
'active', COALESCE(ps.active, p.active),
'visibility', COALESCE(ps.visibility, p.visibility),
'manufacturer', m.name,
'category', cl.name,
/* ================= IMAGE ================= */
'cover_image', JSON_OBJECT(
'id', i.id_image,
'legend', il.legend
),
/* ================= FEATURES ================= */
'features', (
SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'name', fl.name,
'value', fvl.value
)
)
FROM ps_feature_product fp
JOIN ps_feature_lang fl
ON fl.id_feature = fp.id_feature AND fl.id_lang = p_id_lang
JOIN ps_feature_value_lang fvl
ON fvl.id_feature_value = fp.id_feature_value AND fvl.id_lang = p_id_lang
WHERE fp.id_product = p.id_product
),
/* ================= COMBINATIONS ================= */
'combinations', (
SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'id_product_attribute', pa.id_product_attribute,
'reference', pa.reference,
'price', JSON_OBJECT(
'impact', COALESCE(pas.price, pa.price),
'final_tax_excl',
(
COALESCE(ps.price, p.price)
+ COALESCE(pas.price, pa.price)
),
'final_tax_incl',
(
(
COALESCE(ps.price, p.price)
+ COALESCE(pas.price, pa.price)
) * (1 + v_tax_rate / 100)
)
),
'stock', IFNULL(sa.quantity, 0),
'default_on', pas.default_on,
/* ATTRIBUTES JSON */
'attributes', (
SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'group', agl.name,
'attribute', al.name
)
)
FROM ps_product_attribute_combination pac
JOIN ps_attribute a ON a.id_attribute = pac.id_attribute
JOIN ps_attribute_lang al
ON al.id_attribute = a.id_attribute AND al.id_lang = p_id_lang
JOIN ps_attribute_group_lang agl
ON agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = p_id_lang
WHERE pac.id_product_attribute = pa.id_product_attribute
),
/* IMAGES */
'images', (
SELECT JSON_ARRAYAGG(img.id_image)
FROM ps_product_attribute_image pai
JOIN ps_image img ON img.id_image = pai.id_image
WHERE pai.id_product_attribute = pa.id_product_attribute
)
)
)
FROM ps_product_attribute pa
JOIN ps_product_attribute_shop pas
ON pas.id_product_attribute = pa.id_product_attribute
AND pas.id_shop = p_id_shop
LEFT JOIN ps_stock_available sa
ON sa.id_product = pa.id_product
AND sa.id_product_attribute = pa.id_product_attribute
AND sa.id_shop = p_id_shop
WHERE pa.id_product = p.id_product
)
) AS product_json
FROM ps_product p
LEFT JOIN ps_product_shop ps
ON ps.id_product = p.id_product AND ps.id_shop = p_id_shop
LEFT JOIN ps_product_lang pl
ON pl.id_product = p.id_product
AND pl.id_lang = p_id_lang
AND pl.id_shop = p_id_shop
LEFT JOIN ps_category_lang cl
ON cl.id_category = COALESCE(ps.id_category_default, p.id_category_default)
AND cl.id_lang = p_id_lang
AND cl.id_shop = p_id_shop
LEFT JOIN ps_manufacturer m
ON m.id_manufacturer = p.id_manufacturer
LEFT JOIN ps_image i
ON i.id_product = p.id_product AND i.cover = 1
LEFT JOIN ps_image_lang il
ON il.id_image = i.id_image AND il.id_lang = p_id_lang
/* SPECIFIC PRICE */
LEFT JOIN (
SELECT sp1.*
FROM ps_specific_price sp1
WHERE sp1.id_product = p_id_product
AND (sp1.id_customer = 0 OR sp1.id_customer = p_id_customer)
AND (sp1.id_group = 0 OR sp1.id_group = p_id_group)
AND (sp1.id_currency = 0 OR sp1.id_currency = p_id_currency)
AND sp1.from_quantity <= p_quantity
ORDER BY
sp1.id_customer DESC,
sp1.id_group DESC,
sp1.from_quantity DESC,
sp1.id_specific_price DESC
LIMIT 1
) sp ON sp.id_product = p.id_product
WHERE p.id_product = p_id_product
LIMIT 1;
END //
DELIMITER ;
-- +goose Down

431
package-lock.json generated
View File

@@ -4,6 +4,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "b2b",
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.5.1", "@nuxt/ui": "^4.5.1",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
@@ -1006,6 +1007,395 @@
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -1975,13 +2365,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@unhead/vue": { "node_modules/@unhead/vue": {
"version": "2.1.10", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.10.tgz", "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.12.tgz",
"integrity": "sha512-VP78Onh2HNezLPfhYjfHqn4dxlcQsE6PJgTTs61NksO/thvilNswtgBq0N0MWCLtn43N5akEPGW2y2zxM3PWgQ==", "integrity": "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hookable": "^6.0.1", "hookable": "^6.0.1",
"unhead": "2.1.10" "unhead": "2.1.12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/harlan-zw" "url": "https://github.com/sponsors/harlan-zw"
@@ -2801,6 +3191,21 @@
} }
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/fuse.js": { "node_modules/fuse.js": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
@@ -2846,9 +3251,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/h3": { "node_modules/h3": {
"version": "1.15.5", "version": "1.15.8",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.8.tgz",
"integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", "integrity": "sha512-iOH6Vl8mGd9nNfu9C0IZ+GuOAfJHcyf3VriQxWaSWIB76Fg4BnFuk4cxBxjmQSSxJS664+pgjP6e7VBnUzFfcg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
@@ -4257,9 +4662,9 @@
} }
}, },
"node_modules/unhead": { "node_modules/unhead": {
"version": "2.1.10", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.10.tgz", "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz",
"integrity": "sha512-We8l9uNF8zz6U8lfQaVG70+R/QBfQx1oPIgXin4BtZnK2IQpz6yazQ0qjMNVBDw2ADgF2ea58BtvSK+XX5AS7g==", "integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hookable": "^6.0.1" "hookable": "^6.0.1"
@@ -4269,9 +4674,9 @@
} }
}, },
"node_modules/unhead/node_modules/hookable": { "node_modules/unhead/node_modules/hookable": {
"version": "6.0.1", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
"integrity": "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==", "integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unifont": { "node_modules/unifont": {

View File

@@ -1,58 +0,0 @@
package listProductsRepo
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/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UIListProductsRepo interface {
GetListing(p find.Paging, filt *filters.FiltersList) (find.Found[model.Product], error)
}
type ListProductsRepo struct{}
func New() UIListProductsRepo {
return &ListProductsRepo{}
}
func (repo *ListProductsRepo) GetListing(p find.Paging, filt *filters.FiltersList) (find.Found[model.Product], error) {
var listing []model.Product
var total int64
// Apply filters here
q := db.DB.Table("ps_product")
// var resultIDs []uint
// q := db.DB.
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
// // MySQL. It works when followed by `SELECT FOUND_ROWS();`. To learn
// // more see: https://mariarawmodel.com/kb/en/found_rows/
// // WARN: This might not work on different SQL databases
// Select("DISTINCT SQL_CALC_FOUND_ROWS id").
// // Debug().
// Scopes(view.FromDBViewForDisplay(langID, countryIso)).
// Scopes(scopesForFiltersOnDisplay(db.DB, langID, countryIso, filt)).
// Scopes(filt.OfCategory(filters.ORDER_FILTER)...).
// Limit(p.Limit()).
// Offset(p.Offset())
err := q.Count(&total).Error
if err != nil {
return find.Found[model.Product]{}, err
}
err = q.
Limit(p.Limit()).
Offset(p.Offset()).
Scan(&listing).Error
if err != nil {
return find.Found[model.Product]{}, err
}
return find.Found[model.Product]{
Items: listing,
Count: uint(total),
}, nil
}