Compare commits
14 Commits
99fe11fbeb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfc488bad8 | ||
|
|
f5f23f8a27 | ||
| cffe4c2f83 | |||
| 15e8626280 | |||
|
|
528f12b065 | ||
|
|
25ad592be3 | ||
|
|
26e6a3c384 | ||
| a4c1773415 | |||
| 8e07daac66 | |||
| 6408b93e5c | |||
| 27fa88b076 | |||
|
|
b67c4e3aef | ||
|
|
0d29d8f6a2 | ||
|
|
884e15bb8a |
8
.env
8
.env
@@ -21,6 +21,13 @@ 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
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_KEY=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
|
||||||
|
|
||||||
# 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
|
||||||
@@ -29,6 +36,7 @@ GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
|||||||
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
||||||
OAUTH_GOOGLE_CLIENT_SECRET=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
|
OAUTH_GOOGLE_CLIENT_SECRET=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
|
||||||
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
||||||
|
|
||||||
# Email Configuration (SMTP)
|
# Email Configuration (SMTP)
|
||||||
# Set EMAIL_ENABLED=true to require email verification
|
# Set EMAIL_ENABLED=true to require email verification
|
||||||
EMAIL_ENABLED=true
|
EMAIL_ENABLED=true
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
2995
ADD_THIS_TO_SQL.sql
Normal file
File diff suppressed because it is too large
Load Diff
12
Taskfile.yml
12
Taskfile.yml
@@ -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:
|
||||||
|
|||||||
@@ -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)))
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
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"
|
||||||
@@ -52,20 +50,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
|||||||
// "override_currency": c.Query("override_currency", ""),
|
// "override_currency": c.Query("override_currency", ""),
|
||||||
// }
|
// }
|
||||||
|
|
||||||
id_shop_attribute := c.Query("shopID")
|
id_lang, ok := c.Locals("langID").(uint)
|
||||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
if !ok {
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
listing, err := h.listProductsService.GetListing(id_lang, paging, filters)
|
||||||
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)
|
|
||||||
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)))
|
||||||
|
|||||||
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
52
app/delivery/web/api/restricted/localeSelector.go
Normal 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)))
|
||||||
|
}
|
||||||
63
app/delivery/web/api/restricted/meiliSearch.go
Normal file
63
app/delivery/web/api/restricted/meiliSearch.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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, 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.meiliService.CreateIndex(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(¬hing, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
|
||||||
|
id_lang, 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
test, err := h.meiliService.Test(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)))
|
||||||
|
}
|
||||||
45
app/delivery/web/api/restricted/menu.go
Normal file
45
app/delivery/web/api/restricted/menu.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||||
|
id_lang, 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(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)))
|
||||||
|
}
|
||||||
@@ -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)))
|
||||||
|
|||||||
@@ -97,10 +97,18 @@ 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)
|
||||||
|
|
||||||
// // Restricted routes example
|
// // Restricted routes example
|
||||||
// restricted := s.api.Group("/restricted")
|
// restricted := s.api.Group("/restricted")
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ type Product struct {
|
|||||||
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 {
|
type ProductInList struct {
|
||||||
ID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
|
||||||
Name string `gorm:"column:name;default:'no name'" json:"name" form:"name"`
|
Name string `gorm:"column:name" json:"name" form:"name"`
|
||||||
Price float64 `gorm:"column:price;default:0.0" json:"price" form:"price"`
|
ImageID uint `gorm:"column:id_image"`
|
||||||
ActiveAsProduct uint `gorm:"column:active;default:0" json:"active_as_product" form:"active_as_product"`
|
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||||
ActiveInShop uint `gorm:"column:active;default:0" json:"active_in_shop" form:"active_in_shop"`
|
Active uint `gorm:"column:active" json:"active" form:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductFilters struct {
|
type ProductFilters struct {
|
||||||
@@ -81,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
|
||||||
|
|||||||
@@ -18,3 +18,27 @@ 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 ProductRow struct {
|
||||||
|
IDProduct int `gorm:"column:id_product"`
|
||||||
|
IDShop int `gorm:"column:id_shop"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Active uint8 `gorm:"column:active"`
|
||||||
|
Reference string `gorm:"column:reference"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeiliSearchProduct struct {
|
||||||
|
ProductID uint `gorm:"column:id_product"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Active uint8 `gorm:"column:active"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
Description string `gorm:"column:description"`
|
||||||
|
DescriptionShort string `gorm:"column:description_short"`
|
||||||
|
Usage string `gorm:"column:usage"`
|
||||||
|
EAN13 string `gorm:"column:ean13"`
|
||||||
|
Reference string `gorm:"column:reference"`
|
||||||
|
Width float64 `gorm:"column:width"`
|
||||||
|
Height float64 `gorm:"column:height"`
|
||||||
|
Depth float64 `gorm:"column:depth"`
|
||||||
|
Weight float64 `gorm:"column:weight"`
|
||||||
|
}
|
||||||
|
|||||||
41
app/repos/categoriesRepo/categoriesRepo.go
Normal file
41
app/repos/categoriesRepo/categoriesRepo.go
Normal 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
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ package listProductsRepo
|
|||||||
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"
|
||||||
|
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/filters"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIListProductsRepo interface {
|
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{}
|
type ListProductsRepo struct{}
|
||||||
@@ -17,19 +18,10 @@ func New() UIListProductsRepo {
|
|||||||
return &ListProductsRepo{}
|
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 listing []model.ProductInList
|
||||||
var total int64
|
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
|
// var resultIDs []uint
|
||||||
// q := db.DB.
|
// q := db.DB.
|
||||||
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
// // 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()).
|
// Limit(p.Limit()).
|
||||||
// Offset(p.Offset())
|
// 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 {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return find.Found[model.ProductInList]{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = q.
|
err = db.DB.Raw(`
|
||||||
Limit(p.Limit()).
|
SELECT COUNT(*)
|
||||||
Offset(p.Offset()).
|
FROM ps_product`).
|
||||||
Scan(&listing).Error
|
Scan(&total).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return find.Found[model.ProductInList]{}, err
|
||||||
}
|
}
|
||||||
@@ -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").
|
||||||
108
app/repos/productDescriptionRepo/productDescriptionRepo.go
Normal file
108
app/repos/productDescriptionRepo/productDescriptionRepo.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package productDescriptionRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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, productLangID uint) (*model.ProductDescription, error)
|
||||||
|
CreateIfDoesNotExist(productID uint, productLangID uint) error
|
||||||
|
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
|
||||||
|
GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductDescriptionRepo struct{}
|
||||||
|
|
||||||
|
func New() UIProductDescriptionRepo {
|
||||||
|
return &ProductDescriptionRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that any user has access to all product descriptions
|
||||||
|
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, constdata.SHOP_ID, productLangID).
|
||||||
|
First(&ProductDescription).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProductDescription, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't exist, returns an error.
|
||||||
|
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
|
||||||
|
record := model.ProductDescription{
|
||||||
|
ProductID: productID,
|
||||||
|
ShopID: constdata.SHOP_ID,
|
||||||
|
LangID: productLangID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updatesIface := make(map[string]interface{}, len(updates))
|
||||||
|
for k, v := range updates {
|
||||||
|
updatesIface[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that any user has access to all product descriptions
|
||||||
|
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
|
||||||
|
var products []model.MeiliSearchProduct
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Select("pl.`usage` AS `usage`").
|
||||||
|
Select(`
|
||||||
|
ps.id_product AS id_product,
|
||||||
|
pl.name AS name,
|
||||||
|
ps.active AS active,
|
||||||
|
ps.price AS price,
|
||||||
|
pl.description AS description,
|
||||||
|
pl.description_short AS description_short,
|
||||||
|
p.ean13 AS ean13,
|
||||||
|
p.reference AS reference,
|
||||||
|
p.width AS width,
|
||||||
|
p.height AS height,
|
||||||
|
p.depth AS depth,
|
||||||
|
p.weight AS weight
|
||||||
|
`).
|
||||||
|
Table("ps_product_shop AS ps").
|
||||||
|
Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||||
|
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
|
||||||
|
Where("ps.id_shop = ?", constdata.SHOP_ID).
|
||||||
|
Scan(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
return products, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,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]
|
var products find.Found[model.ProductInList]
|
||||||
|
|
||||||
// currencyIso := c.Cookies("currency_iso", "")
|
// 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"]
|
// 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 {
|
if err != nil {
|
||||||
return products, err
|
return products, err
|
||||||
}
|
}
|
||||||
|
|||||||
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
26
app/service/localeSelectorService/localeSelectorService.go
Normal 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()
|
||||||
|
}
|
||||||
8
app/service/meiliService/blank.json
Normal file
8
app/service/meiliService/blank.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"products-openai": {
|
||||||
|
"source": "openAi",
|
||||||
|
"model": "text-embedding-3-small",
|
||||||
|
"apiKey": "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",
|
||||||
|
"documentTemplate": "{{doc.Name}} is equipment used for {{doc.Description | truncatewords: 20}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/service/meiliService/meiliService.go
Normal file
170
app/service/meiliService/meiliService.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package meiliService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"github.com/meilisearch/meilisearch-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeiliService struct {
|
||||||
|
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
|
||||||
|
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,
|
||||||
|
productDescriptionRepo: productDescriptionRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================== FOR SUPERADMIN ONLY ====================================
|
||||||
|
func (s *MeiliService) CreateIndex(id_lang uint) error {
|
||||||
|
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
|
||||||
|
|
||||||
|
for i := 0; i < len(products); i++ {
|
||||||
|
products[i].Description, err = cleanHTML(products[i].Description)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].Description: %v\n", products[i].Description)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at description")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].DescriptionShort: %v\n", products[i].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
|
||||||
|
}
|
||||||
|
|
||||||
|
products[i].Usage, err = cleanHTML(products[i].Usage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].Usage: %v\n", products[i].Usage)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at usage")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(products, 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
|
||||||
|
}
|
||||||
92
app/service/menuService/menuService.go
Normal file
92
app/service/menuService/menuService.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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.Children = append(node.Children, 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.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 }
|
||||||
@@ -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"
|
||||||
@@ -70,7 +70,7 @@ func New() *ProductDescriptionService {
|
|||||||
log.Fatalf("productDescriptionService: cannot create Translation client: %v", err)
|
log.Fatalf("productDescriptionService: cannot create Translation client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
openAIClient := openai.NewClient(option.WithAPIKey("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"),
|
openAIClient := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_KEY")),
|
||||||
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
||||||
|
|
||||||
return &ProductDescriptionService{
|
return &ProductDescriptionService{
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
1
bo/components.d.ts
vendored
@@ -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']
|
||||||
|
|||||||
@@ -1,14 +1,56 @@
|
|||||||
<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 { on } from 'events';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
let menu = await getMenu() as NavigationMenuItem[]
|
||||||
|
|
||||||
|
function adaptMenu(menu) {
|
||||||
|
for (const item of menu) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
adaptMenu(item.children);
|
||||||
|
// item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
|
||||||
|
// item.type = 'trigger'
|
||||||
|
// item.icon = 'i-lucide-book-open'
|
||||||
|
// item.onSelect(e){ console.log(e, 'adasdasdas'); }
|
||||||
|
} else {
|
||||||
|
// console.log(item.params);
|
||||||
|
item.to = { name: 'category', params: item.params };
|
||||||
|
item.icon = 'i-lucide-file-text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = adaptMenu(menu.children)
|
||||||
|
|
||||||
|
console.log(menu);
|
||||||
|
|
||||||
|
const items = ref<NavigationMenuItem[][]>([
|
||||||
|
[
|
||||||
|
|
||||||
|
...menu
|
||||||
|
],
|
||||||
|
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
</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">
|
||||||
|
<div class="mt-24 bg-accented w-1/5 rounded-2xl">
|
||||||
|
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4">
|
||||||
|
</UNavigationMenu>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <template #item>{{ item }}</template> -->
|
||||||
|
|
||||||
|
|
||||||
<TopBar/>
|
<TopBar/>
|
||||||
<router-view />
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()
|
||||||
|
// 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
|
||||||
|
|||||||
8
bo/src/router/menu.ts
Normal file
8
bo/src/router/menu.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { useFetchJson } from "@/composable/useFetchJson";
|
||||||
|
|
||||||
|
export const getMenu = async () => {
|
||||||
|
const resp = await useFetchJson('/api/v1/restricted/menu/get-menu');
|
||||||
|
|
||||||
|
return resp.items
|
||||||
|
|
||||||
|
}
|
||||||
21
bo/src/views/CategoryView.vue
Normal file
21
bo/src/views/CategoryView.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
7
bo/src/views/NotFoundView.vue
Normal file
7
bo/src/views/NotFoundView.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
|
|||||||
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,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
ALTER TABLE b2b_tracker_routes
|
CONSTRAINT fk_parent
|
||||||
ADD CONSTRAINT fk_parent
|
FOREIGN KEY (parent_id)
|
||||||
FOREIGN KEY (parent_id) REFERENCES b2b_tracker_routes(id)
|
REFERENCES b2b_tracker_routes(id)
|
||||||
ON DELETE SET NULL;
|
ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
INSERT IGNORE INTO b2b_tracker_routes
|
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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
package productDescriptionRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductDescriptionRepo struct{}
|
|
||||||
|
|
||||||
func New() UIProductDescriptionRepo {
|
|
||||||
return &ProductDescriptionRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We assume that any user has access to all product descriptions
|
|
||||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productShopID 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).
|
|
||||||
First(&ProductDescription).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProductDescription, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it doesn't exist, returns an error.
|
|
||||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
|
|
||||||
record := model.ProductDescription{
|
|
||||||
ProductID: productID,
|
|
||||||
ShopID: productShopID,
|
|
||||||
LangID: productLangID,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.DB.
|
|
||||||
Table("ps_product_lang").
|
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
||||||
FirstOrCreate(&record).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
updatesIface := make(map[string]interface{}, len(updates))
|
|
||||||
for k, v := range updates {
|
|
||||||
updatesIface[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.DB.
|
|
||||||
Table("ps_product_lang").
|
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
||||||
Updates(updatesIface).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user