14 Commits

41 changed files with 735 additions and 221 deletions

4
.env
View File

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

1
.gitignore vendored
View File

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

View File

@@ -73,10 +73,22 @@ vars:
MP_SMTP_AUTH_ALLOW_INSECURE: true
MP_ENABLE_SPAMASSASSIN: postmark
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:
db_data:
mailpit_data:
meilisearch:
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

@@ -52,20 +52,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
// "override_currency": c.Query("override_currency", ""),
// }
id_shop_attribute := c.Query("shopID")
id_shop, err := strconv.Atoi(id_shop_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
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_shop), uint(id_lang), paging, filters)
listing, err := h.listProductsService.GetListing(uint(id_lang), paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(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,47 @@
package restricted
import (
"strconv"
"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)
return r
}
func (h *MenuHandler) GetMenu(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)))
}
menu, err := h.menuService.GetMenu(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(&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)))
}
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, err := strconv.Atoi(productLangID_attribute)
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)))
}
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 {
return c.Status(responseErrors.GetErrorStatus(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)))
}
// 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 {
userID, ok := c.Locals("userID").(uint)
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)))
}
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, err := strconv.Atoi(productLangID_attribute)
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)))
}
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 {
return c.Status(responseErrors.GetErrorStatus(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)))
}
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, err := strconv.Atoi(productFromLangID_attribute)
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)))
}
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 {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -97,10 +97,18 @@ func (s *Server) Setup() error {
listProducts := s.restricted.Group("/list-products")
restricted.ListProductsHandlerRoutes(listProducts)
// changing the JWT cookies routes (restricted)
// in reality it just handles changing user's country and language
langsAndCountries := s.restricted.Group("/langs-and-countries")
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries)
// locale selector (restricted)
// this is basically for changing user's selected language and country
localeSelector := s.restricted.Group("/langs-and-countries")
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)
// // Restricted routes example
// restricted := s.api.Group("/restricted")

View File

@@ -62,11 +62,11 @@ type Product struct {
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
}
type ProductInList struct {
ID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
Name string `gorm:"column:name;default:'no name'" json:"name" form:"name"`
Price float64 `gorm:"column:price;default:0.0" json:"price" form:"price"`
ActiveAsProduct uint `gorm:"column:active;default:0" json:"active_as_product" form:"active_as_product"`
ActiveInShop uint `gorm:"column:active;default:0" json:"active_in_shop" form:"active_in_shop"`
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 {
@@ -81,4 +81,19 @@ type ProductFilters struct {
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"`
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Name string `json:"name" form:"name"`
Active uint `json:"active" form:"active"`
Subcategories []Category `json:"subcategories" form:"subcategories"`
}
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"`
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
}
type MeiliSearchProduct struct {
ProductID uint
Name string
Description string
DescriptionShort string
Usage string
}

View File

@@ -0,0 +1,42 @@
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
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 = ?`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -3,12 +3,13 @@ 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_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
}
type ListProductsRepo struct{}
@@ -17,19 +18,10 @@ func New() UIListProductsRepo {
return &ListProductsRepo{}
}
func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
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
// Apply filters here
q := db.DB.Table("ps_product").
Select("ps_product.id_product AS id_product", "ps_product_lang.name AS name", "ps_product_shop.price AS price", "ps_product.active AS active_as_product", "ps_product_shop.active AS active_in_shop").
Joins("LEFT JOIN ps_product_shop ON ps_product.id_product = ps_product_shop.id_product").
Joins("LEFT JOIN ps_product_lang ON ps_product.id_product = ps_product_lang.id_product").
Where("ps_product_shop.id_shop = ?", id_shop).
Where("ps_product_lang.id_shop = ?", id_shop).
Where("ps_product_lang.id_lang = ?", id_lang)
// var resultIDs []uint
// q := db.DB.
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
@@ -44,15 +36,41 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi
// Limit(p.Limit()).
// Offset(p.Offset())
err := q.Count(&total).Error
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 = q.
Limit(p.Limit()).
Offset(p.Offset()).
Scan(&listing).Error
err = db.DB.Raw(`
SELECT COUNT(*)
FROM ps_product`).
Scan(&total).Error
if err != nil {
return find.Found[model.ProductInList]{}, err
}

View File

@@ -1,22 +1,22 @@
package langsAndCountriesRepo
package localeSelectorRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UILangsAndCountriesRepo interface {
type UILocaleSelectorRepo interface {
GetLanguages() ([]model.Language, error)
GetCountriesAndCurrencies() ([]model.Country, error)
}
type LangsAndCountriesRepo struct{}
type LocaleSelectorRepo struct{}
func New() UILangsAndCountriesRepo {
return &LangsAndCountriesRepo{}
func New() UILocaleSelectorRepo {
return &LocaleSelectorRepo{}
}
func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
var languages []model.Language
err := db.DB.Table("b2b_language").Scan(&languages).Error
@@ -24,7 +24,7 @@ func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
return languages, err
}
func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
var countries []model.Country
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/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error
UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productLangID uint) error
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
}
type ProductDescriptionRepo struct{}
@@ -20,12 +21,12 @@ func New() UIProductDescriptionRepo {
}
// 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
err := db.DB.
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
if err != nil {
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.
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
record := model.ProductDescription{
ProductID: productID,
ShopID: productShopID,
ShopID: constdata.SHOP_ID,
LangID: productLangID,
}
err := db.DB.
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
if err != nil {
return fmt.Errorf("database error: %w", err)
@@ -53,7 +54,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productSho
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 {
return nil
}
@@ -64,7 +65,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint
err := db.DB.
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
if err != nil {
return fmt.Errorf("database error: %w", err)

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
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/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"

View File

@@ -2,9 +2,9 @@ package listProductsService
import (
"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/find"
"git.ma-al.com/goc_daniel/b2b/repository/listProductsRepo"
)
type ListProductsService struct {
@@ -17,7 +17,7 @@ func New() *ListProductsService {
}
}
func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
var products find.Found[model.ProductInList]
// currencyIso := c.Cookies("currency_iso", "")
@@ -30,7 +30,7 @@ func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Pagi
// countryIso = overrides["override_country"]
// }
products, err := s.listProductsRepo.GetListing(id_shop, id_lang, p, filters)
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
if err != nil {
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,155 @@
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 = cleanHTML(products[i].Description)
nextMeiliProduct.DescriptionShort = cleanHTML(products[i].DescriptionShort)
nextMeiliProduct.Usage = cleanHTML(products[i].Usage)
meiliProducts = append(meiliProducts, nextMeiliProduct)
}
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
primaryKey := "product_id"
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 {
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
}
switch v := token.(type) {
case xml.StartElement:
text += "\n"
case xml.EndElement:
case xml.CharData:
text += string(v)
case xml.Comment:
case xml.ProcInst:
case xml.Directive:
}
}
return text
}

View File

@@ -0,0 +1,90 @@
package menuService
import (
"sort"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo
}
func New() *MenuService {
return &MenuService{
categoriesRepo: categoriesRepo.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.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
}
return node
}
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
var normal model.Category
normal.Active = scanned.Active
normal.CategoryID = scanned.CategoryID
normal.Name = scanned.Name
normal.Subcategories = []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"
"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/repos/productDescriptionRepo"
"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/repository/productDescriptionRepo"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"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) {
return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID)
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) {
return s.productDescriptionRepo.GetProductDescription(productID, productLangID)
}
// 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
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
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 {
return err
}
return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates)
return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates)
}
// 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
// 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 {
return nil, err
}

View File

@@ -2,3 +2,4 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
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
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
@@ -135,6 +138,9 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadPaging):
return i18n.T_(c, "error.err_bad_paging")
case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.no_root_found")
default:
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, ErrBadField),
errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging):
errors.Is(err, ErrBadPaging),
errors.Is(err, ErrNoRootFound):
return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict

1
bo/components.d.ts vendored
View File

@@ -11,6 +11,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']

View File

@@ -31,6 +31,9 @@ const authStore = useAuthStore()
<RouterLink :to="{ name: 'cart' }">
Cart
</RouterLink>
<RouterLink :to="{ name: 'cart1' }">
Cart1
</RouterLink>
<RouterLink :to="{ name: 'products-list' }">
Products List
</RouterLink>

View File

@@ -26,7 +26,6 @@
</UButton>
</div>
<!-- Loading Overlay -->
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />

View File

@@ -0,0 +1,66 @@
<template>
<div class="container mx-auto mt-15">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<h2
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
{{ t('Cart Items') }}
</h2>
<div v-if="cartStore.items.length > 0" class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center justify-between p-4 gap-4">
<div class="flex items-center gap-10 flex-1">
<div
class="w-20 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-xl text-gray-400" />
</div>
<div class="flex-1 min-w-0">
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.name }}</p>
<p class="text-gray-500 dark:text-gray-400 text-sm">
{{ t('Qty') }}: {{ item.quantity }} × ${{ item.price.toFixed(2) }}
</p>
</div>
</div>
<div class="text-right flex-shrink-0">
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</p>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-5xl text-gray-300 dark:text-gray-600 mb-4 mx-auto" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
</div>
<div v-if="cartStore.items.length > 0"
class="p-4 border-t border-(--border-light) dark:border-(--border-dark) bg-(--second-light) dark:bg-(--main-dark) flex gap-4 justify-end items-center">
<UButton color="primary" @click="handleContinueToCheckout"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
{{ t('Continue to Checkout') }}
</UButton>
<UButton variant="outline" color="neutral" @click="handleCancel"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
const cartStore = useCartStore()
const { t } = useI18n()
const router = useRouter()
function handleCancel() {
router.back()
}
function handleContinueToCheckout() {
router.push({ name: 'cart' })
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="container mx-auto mt-10">
<div class="container mx-auto mt-15">
<div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">

View File

@@ -1,5 +1,5 @@
<template>
<div class="container mx-auto mt-20 px-4 py-8">
<div class="container mx-auto mt-15">
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
<div class="flex flex-col lg:flex-row gap-8 mb-8">
<div class="flex-1">

View File

@@ -1,5 +1,5 @@
<template>
<div class="container mt-14 mx-auto">
<div class="container mt-15 mx-auto">
<div class="flex md:flex-row flex-col justify-between gap-8 mb-6">
<div class="flex-1">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">

View File

@@ -1,5 +1,5 @@
<template>
<div class="container mx-auto p-6">
<div class="container mx-auto mt-15">
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
<div v-if="loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
@@ -13,35 +13,36 @@
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Product ID
</th>
Image</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Reference
</th>
Product ID</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Price
</th>
Name</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Link</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="product in products" :key="product.product_id"
<tr v-for="product in productsList" :key="product.product_id"
class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.product_id }}
<td class="px-6 py-4 whitespace-nowrap">
<img :src="getImageUrl(product.ImageID, product.LinkRewrite,)" alt="product image"
class="w-16 h-16 object-cover rounded" />
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.reference }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
product.product_id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ product.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.price.toFixed(2) }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600 dark:text-blue-400">
{{ product.LinkRewrite }}
</td>
</tr>
</tbody>
</table>
<div v-if="products.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
No products found
</div>
</div>
@@ -54,36 +55,36 @@ import { useFetchJson } from '@/composable/useFetchJson'
interface Product {
product_id: number
reference: string
price: number
name: string
ImageID: number
LinkRewrite: string
}
interface ApiResponse {
message: string
items: Product[]
count: number
message: string
}
const products = ref<Product[]>([])
const productsList = ref<Product[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
async function fetchProducts() {
function getImageUrl(imageID: number, linkRewrite: string, size: string = 'small_default') {
return `https://www.naluconcept.com/${imageID}-${size}/${linkRewrite}.webp`
}
async function fetchProductList() {
loading.value = true
error.value = null
try {
const response = await useFetchJson<ApiResponse>('/api/v1/restricted/list-products/get-listing?p=3&elems') as unknown as { items: Product[] }
products.value = response.items || []
} catch (e: any) {
error.value = e.message || 'Failed to load products'
console.error('Error fetching products:', e)
const response = await useFetchJson('/api/v1/restricted/list-products/get-listing?p&elems&shopID=1') as ApiResponse
productsList.value = response.items || []
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load products'
console.error(e)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchProducts()
})
onMounted(fetchProductList)
</script>

View File

@@ -30,6 +30,7 @@ const router = createRouter({
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
{ path: 'cart1', component: () => import('../components/customer/Cart1.vue'), name: 'cart1' },
{ path: 'products-list', component: () => import('../components/customer/PageProductsList.vue'), name: 'products-list' },
],
},

View File

@@ -65,6 +65,7 @@ export const useCartStore = defineStore('cart', () => {
}
}
function removeItem(itemId: number) {
const index = items.value.findIndex(i => i.id === itemId)
if (index !== -1) {

View File

@@ -42,7 +42,7 @@ export const useProductStore = defineStore('product', () => {
}
}
async function saveProductDescription() {
async function saveProductDescription() {
try {
const data = await useFetchJson(
`/api/v1/restricted/product-description/save-product-description?productID=1&productShopID=1&productLangID=1`,
@@ -92,6 +92,6 @@ export const useProductStore = defineStore('product', () => {
getProductDescription,
clearCurrentProduct,
saveProductDescription,
translateProductDescription
translateProductDescription,
}
})

1
go.mod
View File

@@ -29,6 +29,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/meilisearch/meilisearch-go v0.36.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect

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-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/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/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=

View File

@@ -6,16 +6,16 @@ CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
path VARCHAR(255) NULL,
component VARCHAR(255) NOT NULL COMMENT 'path to component file',
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
meta JSON DEFAULT '{}' ,
meta JSON DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
parent_id INT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
parent_id INT NULL,
ALTER TABLE b2b_tracker_routes
ADD CONSTRAINT fk_parent
FOREIGN KEY (parent_id) REFERENCES b2b_tracker_routes(id)
ON DELETE SET NULL;
CONSTRAINT fk_parent
FOREIGN KEY (parent_id)
REFERENCES b2b_tracker_routes(id)
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO b2b_tracker_routes
(name, path, component, layout, meta, is_active, sort_order, parent_id)

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);
-- 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
CREATE TABLE IF NOT EXISTS b2b_countries (
id INT AUTO_INCREMENT PRIMARY KEY,