1 Commits

Author SHA1 Message Date
dd806bbb1e feat: create procedure for retrieving products 2026-03-19 15:44:42 +01:00
49 changed files with 689 additions and 4616 deletions

4
.env
View File

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

3
.gitignore vendored
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,60 +0,0 @@
package restricted
import (
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type MenuHandler struct {
menuService *menuService.MenuService
}
func NewMenuHandler() *MenuHandler {
menuService := menuService.New()
return &MenuHandler{
menuService: menuService,
}
}
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler()
r.Get("/get-menu", handler.GetMenu)
r.Get("/get-routes", handler.GetRouting)
return r
}
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
menu, err := h.menuService.GetMenu(lang_id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
}
func (h *MenuHandler) GetRouting(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
menu, err := h.menuService.GetRoutes(lang_id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
}

View File

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

View File

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

View File

@@ -61,13 +61,6 @@ type Product struct {
State uint `gorm:"column:state" json:"state" form:"state"`
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
}
type ProductInList struct {
ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
Name string `gorm:"column:name" json:"name" form:"name"`
ImageID uint `gorm:"column:id_image"`
LinkRewrite string `gorm:"column:link_rewrite"`
Active uint `gorm:"column:active" json:"active" form:"active"`
}
type ProductFilters struct {
Sort string `json:"sort,omitempty" query:"sort,omitempty" example:"price,asc;name,desc"` // sort rule
@@ -81,28 +74,4 @@ type ProductFilters struct {
InStock uint `query:"stock,omitempty"`
}
type ScannedCategory struct {
CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
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

View File

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

View File

@@ -1,21 +0,0 @@
package model
type Route struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(255);not null;unique"`
Path *string `gorm:"type:varchar(255);default:null"`
Component string `gorm:"type:varchar(255);not null;comment:path to component file"`
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"`
Meta *string `gorm:"type:longtext;default:'{}'"`
IsActive *bool `gorm:"type:tinyint;default:1"`
SortOrder *int `gorm:"type:int;default:0"`
ParentID *uint `gorm:"index"`
Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"`
Children []Route `gorm:"foreignKey:ParentID"`
}
func (Route) TableName() string {
return "b2b_routes"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
bo/components.d.ts vendored
View File

@@ -41,7 +41,6 @@ declare module 'vue' {
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']
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']
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']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const authStore = useAuthStore()
@@ -37,74 +36,70 @@ function validate(): FormError[] {
</script>
<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="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_reset_link_sent_notice') }}
</p>
<UButton color="neutral" variant="outline" block @click="goToLogin"
class="dark:text-white text-black cursor-pointer">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.enter_email_for_password_reset') }}
</p>
</div>
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.send_password_reset_link') }}
</UButton>
</UForm>
<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"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin">
<UIcon name="mingcute:arrow-left-line"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
{{ $t('general.back_to_sign_in') }}
</button>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.dont_have_an_account') }}
<button variant="link" size="sm" @click="goToRegister"
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
$t('general.create_account_now') }}
</button>
</p>
</div>
</template>
<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="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
</component>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_reset_link_sent_notice') }}
</p>
<UButton color="neutral" variant="outline" block @click="goToLogin"
class="dark:text-white text-black cursor-pointer">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.enter_email_for_password_reset') }}
</p>
</div>
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.email_address')" name="email" required
class="w-full dark:text-white text-black">
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.send_password_reset_link') }}
</UButton>
</UForm>
<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"
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
@click="goToLogin">
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
{{ $t('general.back_to_sign_in') }}
</button>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.dont_have_an_account') }}
<button variant="link" size="sm" @click="goToRegister"
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
$t('general.create_account_now') }}
</button>
</p>
</div>
</template>
</div>
</div>
</template>

View File

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

View File

@@ -5,7 +5,6 @@ import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter()
const route = useRoute()
@@ -50,84 +49,80 @@ function validate(): FormError[] {
</script>
<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="text-center mb-15">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
<h2 class="text-xl font-semibold dark:text-white text-black">
{{ $t('general.password_updated') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_updated_description') }}
</p>
<UButton block @click="goToLogin" class="dark:text-white text-black">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle"
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.new_password')" name="new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
: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' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" class="mr-2" />
</template>
</UInput>
</UFormField>
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
: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' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" class="mr-2" />
</template>
</UInput>
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.reset_password') }}
</UButton>
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="ghost" @click="goToLogin"
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
<UIcon name="mingcute:arrow-left-line" />
{{ $t('general.back_to_sign_in') }}
</button>
</div>
</UForm>
</template>
<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="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
</component>
<div class="w-full max-w-md flex flex-col gap-4">
<template v-if="submitted">
<div class="text-center flex flex-col gap-4">
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
<h2 class="text-xl font-semibold dark:text-white text-black">
{{ $t('general.password_updated') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('general.password_updated_description') }}
</p>
<UButton block @click="goToLogin" class="dark:text-white text-black">
{{ $t('general.back_to_sign_in') }}
</UButton>
</div>
</template>
<template v-else>
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
<UAlert v-if="authStore.error" color="error" variant="subtle"
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
@close="authStore.clearError" />
<UFormField :label="$t('general.new_password')" name="new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
: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' }">
<template #trailing>
<UIcon color="neutral" variant="link" size="sm"
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
:aria-pressed="showNewPassword" aria-controls="new_password"
@click="showNewPassword = !showNewPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
class="w-full dark:text-white text-black">
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
: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' }">
<template #trailing>
<UIcon color="neutral" variant="ghost" size="sm"
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
</template>
</UInput>
</UFormField>
<UButton type="submit" block :loading="authStore.loading"
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
{{ $t('general.reset_password') }}
</UButton>
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
<button color="neutral" variant="ghost" @click="goToLogin"
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
<UIcon name="mingcute:arrow-left-line" />
{{ $t('general.back_to_sign_in') }}
</button>
</div>
</UForm>
</template>
</div>
</div>
</template>

View File

@@ -4,7 +4,6 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { t, te } = useI18n()
const router = useRouter()
@@ -67,84 +66,81 @@ function goToLogin() {
</script>
<template>
<component :is="Empty || '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">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
<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">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div>
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
<template #header>
<div class="text-center">
<div v-if="verificationInProgress && loading">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.verifying') }}
</h2>
</div>
<div v-else-if="success">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.success_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.success_message') }}
</p>
</div>
<div v-else-if="error">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.error_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.error_message') }}
</p>
</div>
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
<template #header>
<div class="text-center">
<div v-if="verificationInProgress && loading">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.verifying') }}
</h2>
</div>
</template>
<div v-if="success" class="text-center py-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
</div>
<div v-else-if="error" class="text-center py-4">
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
class="mb-4" />
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
</UButton>
</div>
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
</div>
<template #footer>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
$t('general.sign_in')
}}
</button>
<div v-else-if="success">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.success_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.success_message') }}
</p>
</div>
</template>
</UCard>
</div>
<div v-else-if="error">
<div
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
{{ $t('verify_email.error_title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ $t('verify_email.error_message') }}
</p>
</div>
</div>
</template>
<div v-if="success" class="text-center py-4">
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
</div>
<div v-else-if="error" class="text-center py-4">
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
class="mb-4" />
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
</UButton>
</div>
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
</div>
<template #footer>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in')
}}
</button>
</p>
</div>
</template>
</UCard>
</div>
</div>
</component>
</div>
</template>

1
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/gofiber/fiber/v3 v3.1.0
github.com/golang-jwt/jwt/v5 v5.3.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/samber/lo v1.53.0
golang.org/x/crypto v0.48.0

2
go.sum
View File

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

View File

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

View File

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

View File

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

431
package-lock.json generated
View File

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

View File

@@ -1,22 +1,22 @@
package localeSelectorRepo
package langsAndCountriesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UILocaleSelectorRepo interface {
type UILangsAndCountriesRepo interface {
GetLanguages() ([]model.Language, error)
GetCountriesAndCurrencies() ([]model.Country, error)
}
type LocaleSelectorRepo struct{}
type LangsAndCountriesRepo struct{}
func New() UILocaleSelectorRepo {
return &LocaleSelectorRepo{}
func New() UILangsAndCountriesRepo {
return &LangsAndCountriesRepo{}
}
func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
var languages []model.Language
err := db.DB.Table("b2b_language").Scan(&languages).Error
@@ -24,7 +24,7 @@ func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
return languages, err
}
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
var countries []model.Country
err := db.DB.Table("b2b_countries").

View File

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

View File

@@ -5,13 +5,12 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productLangID uint) error
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
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{}
@@ -21,12 +20,12 @@ func New() UIProductDescriptionRepo {
}
// We assume that any user has access to all product descriptions
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) {
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, 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, constdata.SHOP_ID, productLangID).
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)
@@ -36,16 +35,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLa
}
// If it doesn't exist, returns an error.
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
record := model.ProductDescription{
ProductID: productID,
ShopID: constdata.SHOP_ID,
ShopID: productShopID,
LangID: productLangID,
}
err := db.DB.
Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
FirstOrCreate(&record).Error
if err != nil {
return fmt.Errorf("database error: %w", err)
@@ -54,7 +53,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLan
return nil
}
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
if len(updates) == 0 {
return nil
}
@@ -65,7 +64,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
err := db.DB.
Table("ps_product_lang").
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
Updates(updatesIface).Error
if err != nil {
return fmt.Errorf("database error: %w", err)