Compare commits
21 Commits
22e8556c9d
...
routing
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f494d3abd | |||
|
|
528f12b065 | ||
|
|
25ad592be3 | ||
|
|
26e6a3c384 | ||
| a4c1773415 | |||
| 8e07daac66 | |||
| 6408b93e5c | |||
| 27fa88b076 | |||
|
|
b67c4e3aef | ||
|
|
0d29d8f6a2 | ||
|
|
884e15bb8a | ||
|
|
1ea50af96a | ||
|
|
b6bf6ed5c6 | ||
| 7a66d6f429 | |||
| 43f856ee8d | |||
| 506c64e240 | |||
| 718b4d23f1 | |||
|
|
52c17d7017 | ||
|
|
e094865fc7 | ||
|
|
01c8f4333f | ||
|
|
6cebcacb5d |
4
.env
4
.env
@@ -21,6 +21,10 @@ AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
|
|||||||
AUTH_JWT_EXPIRATION=86400
|
AUTH_JWT_EXPIRATION=86400
|
||||||
AUTH_REFRESH_EXPIRATION=604800
|
AUTH_REFRESH_EXPIRATION=604800
|
||||||
|
|
||||||
|
# Meili search
|
||||||
|
MEILISEARCH_URL=http://localhost:7700
|
||||||
|
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||||
|
|
||||||
# Google Translate Client
|
# Google Translate Client
|
||||||
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
||||||
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ bin/
|
|||||||
i18n/*.json
|
i18n/*.json
|
||||||
*_templ.go
|
*_templ.go
|
||||||
tmp/main
|
tmp/main
|
||||||
|
test.go
|
||||||
2995
ADD_THIS_TO_SQL.sql
Normal file
2995
ADD_THIS_TO_SQL.sql
Normal file
File diff suppressed because it is too large
Load Diff
12
Taskfile.yml
12
Taskfile.yml
@@ -73,10 +73,22 @@ vars:
|
|||||||
MP_SMTP_AUTH_ALLOW_INSECURE: true
|
MP_SMTP_AUTH_ALLOW_INSECURE: true
|
||||||
MP_ENABLE_SPAMASSASSIN: postmark
|
MP_ENABLE_SPAMASSASSIN: postmark
|
||||||
MP_VERBOSE: true
|
MP_VERBOSE: true
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:latest
|
||||||
|
container_name: meilisearch
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 7700:7700
|
||||||
|
volumes:
|
||||||
|
- meilisearch:/data.ms
|
||||||
|
environment:
|
||||||
|
MEILI_MASTER_KEY: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
mailpit_data:
|
mailpit_data:
|
||||||
|
meilisearch:
|
||||||
|
|
||||||
|
|
||||||
includes:
|
includes:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func AuthHandlerRoutes(r fiber.Router) fiber.Router {
|
|||||||
r.Post("/reset-password", handler.ResetPassword)
|
r.Post("/reset-password", handler.ResetPassword)
|
||||||
r.Post("/logout", handler.Logout)
|
r.Post("/logout", handler.Logout)
|
||||||
r.Post("/refresh", handler.RefreshToken)
|
r.Post("/refresh", handler.RefreshToken)
|
||||||
|
r.Post("/update-choice", handler.UpdateJWTToken)
|
||||||
|
|
||||||
// Google OAuth2
|
// Google OAuth2
|
||||||
r.Get("/google", handler.GoogleLogin)
|
r.Get("/google", handler.GoogleLogin)
|
||||||
@@ -344,6 +345,11 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusCreated).JSON(response)
|
return c.Status(fiber.StatusCreated).JSON(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompleteRegistration handles completion of registration with password
|
||||||
|
func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
|
||||||
|
return h.UpdateJWTToken(c)
|
||||||
|
}
|
||||||
|
|
||||||
// GoogleLogin redirects the user to Google's OAuth2 consent page
|
// GoogleLogin redirects the user to Google's OAuth2 consent page
|
||||||
func (h *AuthHandler) GoogleLogin(c fiber.Ctx) error {
|
func (h *AuthHandler) GoogleLogin(c fiber.Ctx) error {
|
||||||
// Generate a random state token and store it in a short-lived cookie
|
// Generate a random state token and store it in a short-lived cookie
|
||||||
@@ -408,9 +414,12 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
|||||||
|
|
||||||
// Redirect to the locale-prefixed charts page after successful Google login.
|
// Redirect to the locale-prefixed charts page after successful Google login.
|
||||||
// The user's preferred language is stored in the auth response; fall back to "en".
|
// The user's preferred language is stored in the auth response; fall back to "en".
|
||||||
lang := response.User.Lang
|
lang, err := h.authService.GetLangISOCode(response.User.LangID)
|
||||||
if lang == "" {
|
if err != nil {
|
||||||
lang = "en"
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadLangID)).JSON(fiber.Map{
|
||||||
|
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang)
|
return c.Redirect().To(h.config.App.BaseURL + "/" + lang)
|
||||||
}
|
}
|
||||||
|
|||||||
128
app/delivery/web/api/restricted/listProducts.go
Normal file
128
app/delivery/web/api/restricted/listProducts.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||||
|
"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"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
|
||||||
|
type ListProductsHandler struct {
|
||||||
|
listProductsService *listProductsService.ListProductsService
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListProductsHandler creates a new ListProductsHandler instance
|
||||||
|
func NewListProductsHandler() *ListProductsHandler {
|
||||||
|
listProductsService := listProductsService.New()
|
||||||
|
return &ListProductsHandler{
|
||||||
|
listProductsService: listProductsService,
|
||||||
|
config: config.Get(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewListProductsHandler()
|
||||||
|
|
||||||
|
r.Get("/get-listing", handler.GetListing)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||||
|
paging, filters, err := ParseProductFilters(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrides := map[string]string{
|
||||||
|
// "override_country": c.Query("override_country", ""),
|
||||||
|
// "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)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnMapping map[string]string = map[string]string{}
|
||||||
|
|
||||||
|
// var columnMapping map[string]string = map[string]string{
|
||||||
|
// "product_id": "id",
|
||||||
|
// "price": "price_taxed",
|
||||||
|
// "name": "name",
|
||||||
|
// "category_id": "category_id",
|
||||||
|
// "feature_id": "feature_id",
|
||||||
|
// "feature": "feature_name",
|
||||||
|
// "value_id": "value_id",
|
||||||
|
// "value": "value_name",
|
||||||
|
// "status": "active_sale",
|
||||||
|
// "stock": "in_stock",
|
||||||
|
// }
|
||||||
|
|
||||||
|
func ParseProductFilters(c fiber.Ctx) (find.Paging, *filters.FiltersList, error) {
|
||||||
|
var p find.Paging
|
||||||
|
fl := filters.NewFiltersList()
|
||||||
|
// productFilters := new(model.ProductFilters)
|
||||||
|
|
||||||
|
// err := c.Bind().Query(productFilters)
|
||||||
|
// if err != nil {
|
||||||
|
// return p, &fl, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if productFilters.Name != "" {
|
||||||
|
// fl.Append(filters.Where("name LIKE ?", fmt.Sprintf("%%%s%%", productFilters.Name)))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if productFilters.Sort != "" {
|
||||||
|
// ord, err := query_params.ParseOrdering[model.Product](c, columnMapping)
|
||||||
|
// if err != nil {
|
||||||
|
// return p, &fl, err
|
||||||
|
// }
|
||||||
|
// for _, o := range ord {
|
||||||
|
// fl.Append(filters.Order(o.Column, o.IsDesc))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if len(productFilters.Features) > 0 {
|
||||||
|
// fl.Append(featureValueFilters(productFilters.Features))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fl.Append(query_params.ParseWhereScopes[model.Product](c, []string{"name"}, columnMapping)...)
|
||||||
|
|
||||||
|
pageNum, pageElems := query_params.ParsePagination(c)
|
||||||
|
p = find.Paging{Page: pageNum, Elements: pageElems}
|
||||||
|
|
||||||
|
return p, &fl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatVal = map[uint][]uint
|
||||||
|
|
||||||
|
func featureValueFilters(feats FeatVal) filters.Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("value_id IN ?", lo.Flatten(lo.Values(feats))).Group("id").Having("COUNT(id) = ?", len(lo.Keys(feats)))
|
||||||
|
}
|
||||||
|
return filters.NewFilter(filters.FEAT_VAL_PRODUCT_FILTER, filt)
|
||||||
|
}
|
||||||
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocaleSelectorHandler for getting languages and countries data
|
||||||
|
type LocaleSelectorHandler struct {
|
||||||
|
localeSelectorService *localeSelectorService.LocaleSelectorService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocaleSelectorHandler creates a new LocaleSelectorHandler instance
|
||||||
|
func NewLocaleSelectorHandler() *LocaleSelectorHandler {
|
||||||
|
localeSelectorService := localeSelectorService.New()
|
||||||
|
return &LocaleSelectorHandler{
|
||||||
|
localeSelectorService: localeSelectorService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewLocaleSelectorHandler()
|
||||||
|
|
||||||
|
r.Get("/get-languages", handler.GetLanguages)
|
||||||
|
r.Get("/get-countries", handler.GetCountries)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
|
||||||
|
languages, err := h.localeSelectorService.GetLanguages()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
|
||||||
|
countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
65
app/delivery/web/api/restricted/meiliSearch.go
Normal file
65
app/delivery/web/api/restricted/meiliSearch.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeiliSearchHandler struct {
|
||||||
|
meiliService *meiliService.MeiliService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMeiliSearchHandler() *MeiliSearchHandler {
|
||||||
|
meiliService := meiliService.New()
|
||||||
|
return &MeiliSearchHandler{
|
||||||
|
meiliService: meiliService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewMeiliSearchHandler()
|
||||||
|
|
||||||
|
r.Get("/test", handler.Test)
|
||||||
|
r.Get("/create-index", handler.CreateIndex)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||||
|
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.meiliService.CreateIndex(uint(id_lang))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
nothing := ""
|
||||||
|
return c.JSON(response.Make(¬hing, 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)))
|
||||||
|
}
|
||||||
60
app/delivery/web/api/restricted/menu.go
Normal file
60
app/delivery/web/api/restricted/menu.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuHandler struct {
|
||||||
|
menuService *menuService.MenuService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMenuHandler() *MenuHandler {
|
||||||
|
menuService := menuService.New()
|
||||||
|
return &MenuHandler{
|
||||||
|
menuService: menuService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewMenuHandler()
|
||||||
|
|
||||||
|
r.Get("/get-menu", handler.GetMenu)
|
||||||
|
r.Get("/get-routes", handler.GetRouting)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||||
|
lang_id, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
menu, err := h.menuService.GetMenu(lang_id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) GetRouting(c fiber.Ctx) error {
|
||||||
|
lang_id, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
menu, err := h.menuService.GetRoutes(lang_id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
@@ -54,13 +54,6 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productLangID_attribute := c.Query("productLangID")
|
productLangID_attribute := c.Query("productLangID")
|
||||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,17 +61,16 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID))
|
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productLangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
"error": responseErrors.GetErrorCode(c, err),
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveProductDescription saves the description for a given product ID, in given shop and language
|
// SaveProductDescription saves the description for a given product ID, in given language
|
||||||
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||||
userID, ok := c.Locals("userID").(uint)
|
userID, ok := c.Locals("userID").(uint)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -93,13 +85,6 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productLangID_attribute := c.Query("productLangID")
|
productLangID_attribute := c.Query("productLangID")
|
||||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,7 +98,7 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates)
|
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
@@ -137,13 +122,6 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productFromLangID_attribute := c.Query("productFromLangID")
|
productFromLangID_attribute := c.Query("productFromLangID")
|
||||||
productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
|
productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,7 +142,7 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel)
|
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
|||||||
@@ -89,10 +89,31 @@ func (s *Server) Setup() error {
|
|||||||
auth := s.public.Group("/auth")
|
auth := s.public.Group("/auth")
|
||||||
public.AuthHandlerRoutes(auth)
|
public.AuthHandlerRoutes(auth)
|
||||||
|
|
||||||
// Repo routes (restricted)
|
// product description routes (restricted)
|
||||||
productDescription := s.restricted.Group("/product-description")
|
productDescription := s.restricted.Group("/product-description")
|
||||||
restricted.ProductDescriptionHandlerRoutes(productDescription)
|
restricted.ProductDescriptionHandlerRoutes(productDescription)
|
||||||
|
|
||||||
|
// listing products routes (restricted)
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
// // Restricted routes example
|
// // Restricted routes example
|
||||||
// restricted := s.api.Group("/restricted")
|
// restricted := s.api.Group("/restricted")
|
||||||
// restricted.Use(middleware.AuthMiddleware())
|
// restricted.Use(middleware.AuthMiddleware())
|
||||||
|
|||||||
11
app/model/countries.go
Normal file
11
app/model/countries.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Represents a country together with its associated currency
|
||||||
|
type Country struct {
|
||||||
|
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||||
|
Name string `gorm:"column:name" json:"name"`
|
||||||
|
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
||||||
|
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
|
||||||
|
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"`
|
||||||
|
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ type Customer struct {
|
|||||||
PasswordResetExpires *time.Time `json:"-"`
|
PasswordResetExpires *time.Time `json:"-"`
|
||||||
LastPasswordResetRequest *time.Time `json:"-"`
|
LastPasswordResetRequest *time.Time `json:"-"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||||
Lang string `gorm:"size:10;default:'en'" json:"lang"` // User's preferred language
|
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
|
||||||
|
CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
@@ -76,9 +77,8 @@ type UserSession struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role CustomerRole `json:"role"`
|
Role CustomerRole `json:"role"`
|
||||||
FirstName string `json:"first_name"`
|
LangID uint `json:"lang_id"`
|
||||||
LastName string `json:"last_name"`
|
CountryID uint `json:"country_id"`
|
||||||
Lang string `json:"lang"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSession converts User to UserSession
|
// ToSession converts User to UserSession
|
||||||
@@ -87,9 +87,8 @@ func (u *Customer) ToSession() *UserSession {
|
|||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
FirstName: u.FirstName,
|
LangID: u.LangID,
|
||||||
LastName: u.LastName,
|
CountryID: u.CountryID,
|
||||||
Lang: u.Lang,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +106,8 @@ type RegisterRequest struct {
|
|||||||
ConfirmPassword string `json:"confirm_password" form:"confirm_password"`
|
ConfirmPassword string `json:"confirm_password" form:"confirm_password"`
|
||||||
FirstName string `json:"first_name" form:"first_name"`
|
FirstName string `json:"first_name" form:"first_name"`
|
||||||
LastName string `json:"last_name" form:"last_name"`
|
LastName string `json:"last_name" form:"last_name"`
|
||||||
Lang string `form:"lang" json:"lang"`
|
LangID uint `form:"lang_id" json:"lang_id"`
|
||||||
|
CountryID uint `form:"country_id" json:"country_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompleteRegistrationRequest represents the completion of registration with email verification
|
// CompleteRegistrationRequest represents the completion of registration with email verification
|
||||||
|
|||||||
108
app/model/product.go
Normal file
108
app/model/product.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Product contains each and every column from the table ps_product.
|
||||||
|
type Product struct {
|
||||||
|
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
||||||
|
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
|
||||||
|
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
|
||||||
|
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
|
||||||
|
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
|
||||||
|
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
|
||||||
|
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
|
||||||
|
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
|
||||||
|
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
|
||||||
|
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
|
||||||
|
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
|
||||||
|
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
|
||||||
|
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
|
||||||
|
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
|
||||||
|
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
|
||||||
|
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
|
||||||
|
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
|
||||||
|
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
|
||||||
|
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
|
||||||
|
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
|
||||||
|
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
|
||||||
|
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
|
||||||
|
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
|
||||||
|
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
|
||||||
|
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
|
||||||
|
|
||||||
|
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
|
||||||
|
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
|
||||||
|
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
|
||||||
|
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
|
||||||
|
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
|
||||||
|
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
|
||||||
|
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
|
||||||
|
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
|
||||||
|
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
|
||||||
|
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
|
||||||
|
|
||||||
|
Active uint `gorm:"column:active" json:"active" form:"active"`
|
||||||
|
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
|
||||||
|
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
|
||||||
|
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
|
||||||
|
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
|
||||||
|
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
|
||||||
|
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
|
||||||
|
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
|
||||||
|
|
||||||
|
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
|
||||||
|
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
|
||||||
|
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
|
||||||
|
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
|
||||||
|
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
|
||||||
|
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
|
||||||
|
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
|
||||||
|
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
|
||||||
|
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
|
||||||
|
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
|
||||||
|
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
|
||||||
|
ProductID uint `json:"product_id,omitempty" query:"product_id,omitempty" example:"1"`
|
||||||
|
Price float64 `json:"price,omitempty" query:"price,omitempty" example:"123.45"`
|
||||||
|
Name string `json:"name,omitempty" query:"name,omitempty" example:"Sztabka Złota Britannia"`
|
||||||
|
CategoryID uint `json:"category_id,omitempty" query:"category_id,omitempty" example:"2"`
|
||||||
|
CategoryName string `json:"category_name,omitempty" query:"category_name,omitempty" example:"Złote Monety"`
|
||||||
|
Features FeatVal `query:"features,omitempty"`
|
||||||
|
ActiveSale bool `query:"sale_active,omitempty"`
|
||||||
|
InStock uint `query:"stock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScannedCategory struct {
|
||||||
|
CategoryID uint `gorm:"column:ID;primaryKey"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Active uint `gorm:"column:active"`
|
||||||
|
Position uint `gorm:"column:position"`
|
||||||
|
ParentID uint `gorm:"column:id_parent"`
|
||||||
|
IsRoot uint `gorm:"column:is_root_category"`
|
||||||
|
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||||
|
IsoCode string `gorm:"column:iso_code"`
|
||||||
|
}
|
||||||
|
type Category struct {
|
||||||
|
CategoryID uint `json:"category_id" form:"category_id"`
|
||||||
|
Label string `json:"label" form:"label"`
|
||||||
|
// Active bool `json:"active" form:"active"`
|
||||||
|
Params CategpryParams `json:"params" form:"params"`
|
||||||
|
Children []Category `json:"children" form:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategpryParams struct {
|
||||||
|
CategoryID uint `json:"category_id" form:"category_id"`
|
||||||
|
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
|
||||||
|
Locale string `json:"locale" form:"locale"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatVal = map[uint][]uint
|
||||||
@@ -18,3 +18,11 @@ type ProductDescription struct {
|
|||||||
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
||||||
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MeiliSearchProduct struct {
|
||||||
|
ProductID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
DescriptionShort string
|
||||||
|
Usage string
|
||||||
|
}
|
||||||
|
|||||||
21
app/model/routing.go
Normal file
21
app/model/routing.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `gorm:"type:varchar(255);not null;unique"`
|
||||||
|
Path *string `gorm:"type:varchar(255);default:null"`
|
||||||
|
Component string `gorm:"type:varchar(255);not null;comment:path to component file"`
|
||||||
|
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"`
|
||||||
|
Meta *string `gorm:"type:longtext;default:'{}'"`
|
||||||
|
IsActive *bool `gorm:"type:tinyint;default:1"`
|
||||||
|
SortOrder *int `gorm:"type:int;default:0"`
|
||||||
|
|
||||||
|
ParentID *uint `gorm:"index"`
|
||||||
|
Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"`
|
||||||
|
|
||||||
|
Children []Route `gorm:"foreignKey:ParentID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Route) TableName() string {
|
||||||
|
return "b2b_routes"
|
||||||
|
}
|
||||||
41
app/repos/categoriesRepo/categoriesRepo.go
Normal file
41
app/repos/categoriesRepo/categoriesRepo.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package categoriesRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UICategoriesRepo interface {
|
||||||
|
GetAllCategories(id_lang uint) ([]model.ScannedCategory, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoriesRepo struct{}
|
||||||
|
|
||||||
|
func New() UICategoriesRepo {
|
||||||
|
return &CategoriesRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) {
|
||||||
|
var allCategories []model.ScannedCategory
|
||||||
|
|
||||||
|
err := db.DB.Raw(`
|
||||||
|
SELECT
|
||||||
|
ps_category.id_category AS ID,
|
||||||
|
ps_category_lang.name AS name,
|
||||||
|
ps_category.active AS active,
|
||||||
|
ps_category_shop.position AS position,
|
||||||
|
ps_category.id_parent AS id_parent,
|
||||||
|
ps_category.is_root_category AS is_root_category,
|
||||||
|
ps_category_lang.link_rewrite AS link_rewrite,
|
||||||
|
ps_lang.iso_code AS iso_code
|
||||||
|
FROM ps_category
|
||||||
|
LEFT JOIN ps_category_lang ON ps_category_lang.id_category = ps_category.id_category AND ps_category_lang.id_shop = ? AND ps_category_lang.id_lang = ?
|
||||||
|
LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ?
|
||||||
|
JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang
|
||||||
|
`,
|
||||||
|
constdata.SHOP_ID, id_lang, constdata.SHOP_ID).
|
||||||
|
Scan(&allCategories).Error
|
||||||
|
|
||||||
|
return allCategories, err
|
||||||
|
}
|
||||||
82
app/repos/listProductsRepo/listProductsRepo.go
Normal file
82
app/repos/listProductsRepo/listProductsRepo.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package listProductsRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UIListProductsRepo interface {
|
||||||
|
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProductsRepo struct{}
|
||||||
|
|
||||||
|
func New() UIListProductsRepo {
|
||||||
|
return &ListProductsRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||||
|
var listing []model.ProductInList
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// var resultIDs []uint
|
||||||
|
// q := db.DB.
|
||||||
|
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
||||||
|
// // MySQL. It works when followed by `SELECT FOUND_ROWS();`. To learn
|
||||||
|
// // more see: https://mariarawmodel.com/kb/en/found_rows/
|
||||||
|
// // WARN: This might not work on different SQL databases
|
||||||
|
// Select("DISTINCT SQL_CALC_FOUND_ROWS id").
|
||||||
|
// // Debug().
|
||||||
|
// Scopes(view.FromDBViewForDisplay(langID, countryIso)).
|
||||||
|
// Scopes(scopesForFiltersOnDisplay(db.DB, langID, countryIso, filt)).
|
||||||
|
// Scopes(filt.OfCategory(filters.ORDER_FILTER)...).
|
||||||
|
// Limit(p.Limit()).
|
||||||
|
// Offset(p.Offset())
|
||||||
|
|
||||||
|
err := db.DB.Raw(`
|
||||||
|
SELECT
|
||||||
|
ps_product.id_product AS ID,
|
||||||
|
ps_product_lang.name AS name,
|
||||||
|
ps_product.active AS active,
|
||||||
|
ps_product_lang.link_rewrite AS link_rewrite,
|
||||||
|
COALESCE (
|
||||||
|
ps_image_shop.id_image, any_image.id_image
|
||||||
|
) AS id_image
|
||||||
|
FROM ps_product
|
||||||
|
LEFT JOIN ps_product_lang
|
||||||
|
ON ps_product_lang.id_product = ps_product.id_product
|
||||||
|
AND ps_product_lang.id_shop = ?
|
||||||
|
AND ps_product_lang.id_lang = ?
|
||||||
|
LEFT JOIN ps_image_shop
|
||||||
|
ON ps_image_shop.id_product = ps_product.id_product
|
||||||
|
AND ps_image_shop.id_shop = ?
|
||||||
|
AND ps_image_shop.cover = 1
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT id_product, MIN(id_image) AS id_image
|
||||||
|
FROM ps_image
|
||||||
|
GROUP BY id_product
|
||||||
|
) any_image
|
||||||
|
ON ps_product.id_product = any_image.id_product
|
||||||
|
LIMIT ? OFFSET ?`,
|
||||||
|
constdata.SHOP_ID, id_lang, constdata.SHOP_ID, p.Limit(), p.Offset()).
|
||||||
|
Scan(&listing).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.ProductInList]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.DB.Raw(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM ps_product`).
|
||||||
|
Scan(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.ProductInList]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return find.Found[model.ProductInList]{
|
||||||
|
Items: listing,
|
||||||
|
Count: uint(total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
36
app/repos/localeSelectorRepo/localeSelectorRepo.go
Normal file
36
app/repos/localeSelectorRepo/localeSelectorRepo.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package localeSelectorRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UILocaleSelectorRepo interface {
|
||||||
|
GetLanguages() ([]model.Language, error)
|
||||||
|
GetCountriesAndCurrencies() ([]model.Country, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocaleSelectorRepo struct{}
|
||||||
|
|
||||||
|
func New() UILocaleSelectorRepo {
|
||||||
|
return &LocaleSelectorRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
|
||||||
|
var languages []model.Language
|
||||||
|
|
||||||
|
err := db.DB.Table("b2b_language").Scan(&languages).Error
|
||||||
|
|
||||||
|
return languages, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||||
|
var countries []model.Country
|
||||||
|
|
||||||
|
err := db.DB.Table("b2b_countries").
|
||||||
|
Select("b2b_countries.id, b2b_countries.name, b2b_countries.flag, ps_currency.id as id_currency, ps_currency.name as currency_name, ps_currency.iso_code as currency_iso_code").
|
||||||
|
Joins("JOIN ps_currency ON ps_currency.id = b2b_countries.currency").
|
||||||
|
Scan(&countries).Error
|
||||||
|
|
||||||
|
return countries, err
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package ProductDescriptionRepo
|
package productDescriptionRepo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIProductDescriptionRepo interface {
|
type UIProductDescriptionRepo interface {
|
||||||
GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error)
|
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
|
||||||
CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error
|
CreateIfDoesNotExist(productID uint, productLangID uint) error
|
||||||
UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error
|
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductDescriptionRepo struct{}
|
type ProductDescriptionRepo struct{}
|
||||||
@@ -20,12 +21,12 @@ func New() UIProductDescriptionRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We assume that any user has access to all product descriptions
|
// We assume that any user has access to all product descriptions
|
||||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||||
var ProductDescription model.ProductDescription
|
var ProductDescription model.ProductDescription
|
||||||
|
|
||||||
err := db.DB.
|
err := db.DB.
|
||||||
Table("ps_product_lang").
|
Table("ps_product_lang").
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
First(&ProductDescription).Error
|
First(&ProductDescription).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
@@ -35,16 +36,16 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productSh
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If it doesn't exist, returns an error.
|
// If it doesn't exist, returns an error.
|
||||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
|
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
|
||||||
record := model.ProductDescription{
|
record := model.ProductDescription{
|
||||||
ProductID: productID,
|
ProductID: productID,
|
||||||
ShopID: productShopID,
|
ShopID: constdata.SHOP_ID,
|
||||||
LangID: productLangID,
|
LangID: productLangID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.DB.
|
err := db.DB.
|
||||||
Table("ps_product_lang").
|
Table("ps_product_lang").
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
FirstOrCreate(&record).Error
|
FirstOrCreate(&record).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("database error: %w", err)
|
return fmt.Errorf("database error: %w", err)
|
||||||
@@ -53,7 +54,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productSho
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
|
||||||
if len(updates) == 0 {
|
if len(updates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,7 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint
|
|||||||
|
|
||||||
err := db.DB.
|
err := db.DB.
|
||||||
Table("ps_product_lang").
|
Table("ps_product_lang").
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
Updates(updatesIface).Error
|
Updates(updatesIface).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("database error: %w", err)
|
return fmt.Errorf("database error: %w", err)
|
||||||
25
app/repos/routesRepo/routesRepo.go
Normal file
25
app/repos/routesRepo/routesRepo.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package routesrepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UIRoutesRepo interface {
|
||||||
|
GetRoutes(langId uint) ([]model.Route, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoutesRepo struct{}
|
||||||
|
|
||||||
|
func New() UIRoutesRepo {
|
||||||
|
return &RoutesRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
|
||||||
|
routes := []model.Route{}
|
||||||
|
err := db.DB.Find(&routes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
@@ -13,9 +14,13 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
|
||||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"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"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -27,8 +32,9 @@ type JWTClaims struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role model.CustomerRole `json:"customer_role"`
|
Role model.CustomerRole `json:"customer_role"`
|
||||||
FirstName string `json:"first_name"`
|
CartsIDs []uint `json:"carts_ids"`
|
||||||
LastName string `json:"last_name"`
|
LangID uint `json:"lang_id"`
|
||||||
|
CountryID uint `json:"country_id"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +155,8 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
|||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
EmailVerificationToken: token,
|
EmailVerificationToken: token,
|
||||||
EmailVerificationExpires: &expiresAt,
|
EmailVerificationExpires: &expiresAt,
|
||||||
Lang: req.Lang,
|
LangID: req.LangID,
|
||||||
|
CountryID: req.CountryID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&user).Error; err != nil {
|
if err := s.db.Create(&user).Error; err != nil {
|
||||||
@@ -158,10 +165,11 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
|||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
baseURL := config.Get().App.BaseURL
|
baseURL := config.Get().App.BaseURL
|
||||||
lang := req.Lang
|
lang, err := s.GetLangISOCode(req.LangID)
|
||||||
if lang == "" {
|
if err != nil {
|
||||||
lang = "en" // Default to English
|
return responseErrors.ErrBadLangID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
|
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
|
||||||
// Log error but don't fail registration - user can request resend
|
// Log error but don't fail registration - user can request resend
|
||||||
_ = err
|
_ = err
|
||||||
@@ -266,10 +274,11 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
|
|||||||
|
|
||||||
// Send password reset email
|
// Send password reset email
|
||||||
baseURL := config.Get().App.BaseURL
|
baseURL := config.Get().App.BaseURL
|
||||||
lang := "en"
|
lang, err := s.GetLangISOCode(user.LangID)
|
||||||
if user.Lang != "" {
|
if err != nil {
|
||||||
lang = user.Lang
|
return responseErrors.ErrBadLangID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
|
if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
|
||||||
_ = err
|
_ = err
|
||||||
}
|
}
|
||||||
@@ -471,13 +480,24 @@ func hashToken(raw string) string {
|
|||||||
|
|
||||||
// generateAccessToken generates a short-lived JWT access token
|
// generateAccessToken generates a short-lived JWT access token
|
||||||
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
|
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
|
||||||
|
_, err := s.GetLangISOCode(user.LangID)
|
||||||
|
if err != nil {
|
||||||
|
return "", responseErrors.ErrBadLangID
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.CheckIfCountryExists(user.CountryID)
|
||||||
|
if err != nil {
|
||||||
|
return "", responseErrors.ErrBadCountryID
|
||||||
|
}
|
||||||
|
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
FirstName: user.FirstName,
|
CartsIDs: []uint{},
|
||||||
LastName: user.LastName,
|
LangID: user.LangID,
|
||||||
|
CountryID: user.CountryID,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.JWTExpiration) * time.Second)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.JWTExpiration) * time.Second)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
@@ -488,6 +508,84 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
|
|||||||
return token.SignedString([]byte(s.config.JWTSecret))
|
return token.SignedString([]byte(s.config.JWTSecret))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) UpdateJWTToken(c fiber.Ctx) error {
|
||||||
|
// Get user ID from JWT claims in context (set by auth middleware)
|
||||||
|
claims, ok := c.Locals("jwt_claims").(*JWTClaims)
|
||||||
|
if !ok || claims == nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var user model.Customer
|
||||||
|
// Find user by ID
|
||||||
|
if err := s.db.First(&user, claims.UserID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse language and country_id from query params
|
||||||
|
langIDStr := c.Query("lang_id")
|
||||||
|
|
||||||
|
var langID uint
|
||||||
|
if langIDStr != "" {
|
||||||
|
parsedID, err := strconv.ParseUint(langIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
|
||||||
|
}
|
||||||
|
langID = uint(parsedID)
|
||||||
|
|
||||||
|
_, err = s.GetLangISOCode(langID)
|
||||||
|
if err != nil {
|
||||||
|
return responseErrors.ErrBadLangID
|
||||||
|
} else {
|
||||||
|
user.LangID = langID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countryIDStr := c.Query("country_id")
|
||||||
|
|
||||||
|
var countryID uint
|
||||||
|
if countryIDStr != "" {
|
||||||
|
parsedID, err := strconv.ParseUint(countryIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
|
||||||
|
}
|
||||||
|
countryID = uint(parsedID)
|
||||||
|
|
||||||
|
err = s.CheckIfCountryExists(countryID)
|
||||||
|
if err != nil {
|
||||||
|
return responseErrors.ErrBadCountryID
|
||||||
|
} else {
|
||||||
|
user.CountryID = countryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update choice and get new token using AuthService
|
||||||
|
newToken, err := s.generateAccessToken(&user)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated user
|
||||||
|
if err := s.db.Save(&user).Error; err != nil {
|
||||||
|
return fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new JWT cookie
|
||||||
|
cookie := new(fiber.Cookie)
|
||||||
|
cookie.Name = "jwt_token"
|
||||||
|
cookie.Value = newToken
|
||||||
|
cookie.HTTPOnly = true
|
||||||
|
cookie.Secure = true
|
||||||
|
cookie.SameSite = fiber.CookieSameSiteLaxMode
|
||||||
|
|
||||||
|
c.Cookie(cookie)
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&fiber.Map{"token": newToken}, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
// generateVerificationToken generates a random verification token
|
// generateVerificationToken generates a random verification token
|
||||||
func (s *AuthService) generateVerificationToken() (string, error) {
|
func (s *AuthService) generateVerificationToken() (string, error) {
|
||||||
bytes := make([]byte, 32)
|
bytes := make([]byte, 32)
|
||||||
@@ -507,3 +605,29 @@ func validatePassword(password string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) GetLangISOCode(langID uint) (string, error) {
|
||||||
|
var lang string
|
||||||
|
|
||||||
|
if langID == 0 { // retrieve the default lang
|
||||||
|
err := db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||||
|
return lang, err
|
||||||
|
} else {
|
||||||
|
err := db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||||
|
return lang, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) CheckIfCountryExists(countryID uint) error {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := db.DB.Table("b2b_countries").Where("id = ?", countryID).Count(&count).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return responseErrors.ErrBadCountryID
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
|||||||
Role: model.RoleUser,
|
Role: model.RoleUser,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
Lang: "en",
|
LangID: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Create(&newUser).Error; err != nil {
|
if err := s.db.Create(&newUser).Error; err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package langsService
|
package langsService
|
||||||
|
|
||||||
import (
|
import (
|
||||||
langs_repo "git.ma-al.com/goc_daniel/b2b/app/langs"
|
langs_repo "git.ma-al.com/goc_daniel/b2b/app/repos/langsRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
|||||||
59
app/service/listProductsService/listProductsService.go
Normal file
59
app/service/listProductsService/listProductsService.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListProductsService struct {
|
||||||
|
listProductsRepo listProductsRepo.UIListProductsRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *ListProductsService {
|
||||||
|
return &ListProductsService{
|
||||||
|
listProductsRepo: listProductsRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||||
|
var products find.Found[model.ProductInList]
|
||||||
|
|
||||||
|
// currencyIso := c.Cookies("currency_iso", "")
|
||||||
|
// countryIso := c.Cookies("country_iso", "")
|
||||||
|
|
||||||
|
// if overrides["override_currency"] != "" {
|
||||||
|
// currencyIso = overrides["override_currency"]
|
||||||
|
// }
|
||||||
|
// if overrides["override_country"] != "" {
|
||||||
|
// countryIso = overrides["override_country"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
|
||||||
|
if err != nil {
|
||||||
|
return products, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// var loopErr error
|
||||||
|
// parallel.ForEach(products.Items, func(t model.Product, i int) {
|
||||||
|
// // products.Items[i].PriceTaxed *= currRate.Rate.InexactFloat64()
|
||||||
|
// // products.Items[i].PriceTaxed = tiny_util.RoundUpMonetary(products.Items[i].PriceTaxed)
|
||||||
|
|
||||||
|
// if products.Items[i].Name.IsNull() {
|
||||||
|
// translation, err := s.listProductsRepo.GetTranslation(ctx, products.Items[i].ID, defaults.DefaultLanguageID)
|
||||||
|
// if err != nil {
|
||||||
|
// loopErr = err
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// products.Items[i].Name = nullable.FromPrimitiveString(translation.Name)
|
||||||
|
// products.Items[i].DescriptionShort = nullable.FromPrimitiveString(translation.DescriptionShort)
|
||||||
|
// products.Items[i].LinkRewrite = nullable.FromPrimitiveString(translation.LinkRewrite)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// if loopErr != nil {
|
||||||
|
// return products, errs.Handled(span, loopErr, errs.InternalError, errs.ERR_TODO)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package localeSelectorService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/localeSelectorRepo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocaleSelectorService literally sends back language and countries information.
|
||||||
|
type LocaleSelectorService struct {
|
||||||
|
repo localeSelectorRepo.UILocaleSelectorRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocaleSelectorService creates a new LocaleSelector service
|
||||||
|
func New() *LocaleSelectorService {
|
||||||
|
return &LocaleSelectorService{
|
||||||
|
repo: localeSelectorRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocaleSelectorService) GetLanguages() ([]model.Language, error) {
|
||||||
|
return s.repo.GetLanguages()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocaleSelectorService) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||||
|
return s.repo.GetCountriesAndCurrencies()
|
||||||
|
}
|
||||||
185
app/service/meiliService/meiliService.go
Normal file
185
app/service/meiliService/meiliService.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package meiliService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"github.com/meilisearch/meilisearch-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeiliService struct {
|
||||||
|
meiliClient meilisearch.ServiceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *MeiliService {
|
||||||
|
meiliURL := os.Getenv("MEILISEARCH_URL")
|
||||||
|
meiliAPIKey := os.Getenv("MEILISEARCH_API_KEY")
|
||||||
|
|
||||||
|
client := meilisearch.New(
|
||||||
|
meiliURL,
|
||||||
|
meilisearch.WithAPIKey(meiliAPIKey),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &MeiliService{
|
||||||
|
meiliClient: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================== FOR SUPERADMIN ONLY ====================================
|
||||||
|
func (s *MeiliService) CreateIndex(id_lang uint) error {
|
||||||
|
var products []model.ProductDescription
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
Where("id_shop = ? AND id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||||
|
Scan(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var meiliProducts []model.MeiliSearchProduct
|
||||||
|
for i := 0; i < len(products); i++ {
|
||||||
|
var nextMeiliProduct model.MeiliSearchProduct
|
||||||
|
|
||||||
|
nextMeiliProduct.ProductID = products[i].ProductID
|
||||||
|
nextMeiliProduct.Name = products[i].Name
|
||||||
|
|
||||||
|
nextMeiliProduct.Description, err = cleanHTML(products[i].Description)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("nextMeiliProduct.Description: %v\n", nextMeiliProduct.Description)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at description")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMeiliProduct.DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("nextMeiliProduct.DescriptionShort: %v\n", nextMeiliProduct.DescriptionShort)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at description short")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMeiliProduct.Usage, err = cleanHTML(products[i].Usage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("nextMeiliProduct.Usage: %v\n", nextMeiliProduct.Usage)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at usage")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
meiliProducts = append(meiliProducts, nextMeiliProduct)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||||
|
primaryKey := "ProductID"
|
||||||
|
docOptions := &meilisearch.DocumentOptions{
|
||||||
|
PrimaryKey: &primaryKey,
|
||||||
|
SkipCreation: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := s.meiliClient.Index(indexName).AddDocuments(meiliProducts, docOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||||
|
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||||
|
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================== FOR DEBUG ONLY ====================================
|
||||||
|
func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
|
||||||
|
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||||
|
|
||||||
|
searchReq := &meilisearch.SearchRequest{
|
||||||
|
Limit: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
results, err := s.meiliClient.Index(indexName).Search("walek", searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Meilisearch error: %v\n", err)
|
||||||
|
return meilisearch.SearchResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Search results for query 'walek' in %s: %d hits\n", indexName, len(results.Hits))
|
||||||
|
|
||||||
|
return *results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search performs a full-text search on the specified index
|
||||||
|
func (s *MeiliService) Search(indexName string, query string, limit int) (meilisearch.SearchResponse, error) {
|
||||||
|
searchReq := &meilisearch.SearchRequest{
|
||||||
|
Limit: int64(limit),
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.meiliClient.Index(indexName).Search(query, searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Meilisearch search error: %v\n", err)
|
||||||
|
return meilisearch.SearchResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck checks if Meilisearch is healthy and accessible
|
||||||
|
func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
|
||||||
|
health, err := s.meiliClient.Health()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("meilisearch health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return health, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all tags from HTML text
|
||||||
|
func cleanHTML(s string) (string, error) {
|
||||||
|
r := strings.NewReader(s)
|
||||||
|
d := xml.NewDecoder(r)
|
||||||
|
|
||||||
|
text := ""
|
||||||
|
|
||||||
|
// Configure the decoder for HTML; leave off strict and autoclose for XHTML
|
||||||
|
d.Strict = true
|
||||||
|
d.AutoClose = xml.HTMLAutoClose
|
||||||
|
d.Entity = xml.HTMLEntity
|
||||||
|
for {
|
||||||
|
token, err := d.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return text, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := token.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if len(text) > 0 && text[len(text)-1] != '\n' {
|
||||||
|
text += " \n "
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
case xml.CharData:
|
||||||
|
if strings.TrimSpace(string(v)) != "" {
|
||||||
|
text += string(v)
|
||||||
|
}
|
||||||
|
case xml.Comment:
|
||||||
|
case xml.ProcInst:
|
||||||
|
case xml.Directive:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
99
app/service/menuService/menuService.go
Normal file
99
app/service/menuService/menuService.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package menuService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
|
||||||
|
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuService struct {
|
||||||
|
categoriesRepo categoriesRepo.UICategoriesRepo
|
||||||
|
routesRepo routesRepo.UIRoutesRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *MenuService {
|
||||||
|
return &MenuService{
|
||||||
|
categoriesRepo: categoriesRepo.New(),
|
||||||
|
routesRepo: routesRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||||
|
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
|
||||||
|
if err != nil {
|
||||||
|
return &model.Category{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the root
|
||||||
|
root_index := 0
|
||||||
|
root_found := false
|
||||||
|
for i := 0; i < len(all_categories); i++ {
|
||||||
|
if all_categories[i].IsRoot == 1 {
|
||||||
|
root_index = i
|
||||||
|
root_found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !root_found {
|
||||||
|
return &model.Category{}, responseErrors.ErrNoRootFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// now create the children and reorder them according to position
|
||||||
|
id_to_index := make(map[uint]int)
|
||||||
|
for i := 0; i < len(all_categories); i++ {
|
||||||
|
id_to_index[all_categories[i].CategoryID] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
children_indices := make(map[int][]ChildWithPosition)
|
||||||
|
for i := 0; i < len(all_categories); i++ {
|
||||||
|
parent_index := id_to_index[all_categories[i].ParentID]
|
||||||
|
children_indices[parent_index] = append(children_indices[parent_index], ChildWithPosition{Index: i, Position: all_categories[i].Position})
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range children_indices {
|
||||||
|
sort.Sort(ByPosition(children_indices[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, create the tree
|
||||||
|
tree := s.createTree(root_index, &all_categories, &children_indices)
|
||||||
|
|
||||||
|
return &tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
|
||||||
|
node := s.scannedToNormalCategory((*all_categories)[index])
|
||||||
|
|
||||||
|
for i := 0; i < len((*children_indices)[index]); i++ {
|
||||||
|
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
|
||||||
|
return s.routesRepo.GetRoutes(id_lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
|
||||||
|
var normal model.Category
|
||||||
|
// normal.Active = scanned.Active
|
||||||
|
normal.CategoryID = scanned.CategoryID
|
||||||
|
normal.Label = scanned.Name
|
||||||
|
// normal.Active = scanned.Active == 1
|
||||||
|
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
|
||||||
|
normal.Children = []model.Category{}
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChildWithPosition struct {
|
||||||
|
Index int
|
||||||
|
Position uint
|
||||||
|
}
|
||||||
|
type ByPosition []ChildWithPosition
|
||||||
|
|
||||||
|
func (a ByPosition) Len() int { return len(a) }
|
||||||
|
func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
|
||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
"cloud.google.com/go/translate/apiv3/translatepb"
|
"cloud.google.com/go/translate/apiv3/translatepb"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
ProductDescriptionRepo "git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
|
|
||||||
"github.com/openai/openai-go/v3"
|
"github.com/openai/openai-go/v3"
|
||||||
"github.com/openai/openai-go/v3/option"
|
"github.com/openai/openai-go/v3/option"
|
||||||
"github.com/openai/openai-go/v3/responses"
|
"github.com/openai/openai-go/v3/responses"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductDescriptionService struct {
|
type ProductDescriptionService struct {
|
||||||
productDescriptionRepo ProductDescriptionRepo.ProductDescriptionRepo
|
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
googleCli translate.TranslationClient
|
googleCli translate.TranslationClient
|
||||||
projectID string
|
projectID string
|
||||||
@@ -74,19 +74,20 @@ func New() *ProductDescriptionService {
|
|||||||
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
||||||
|
|
||||||
return &ProductDescriptionService{
|
return &ProductDescriptionService{
|
||||||
ctx: ctx,
|
productDescriptionRepo: productDescriptionRepo.New(),
|
||||||
openAIClient: openAIClient,
|
ctx: ctx,
|
||||||
googleCli: *googleCli,
|
openAIClient: openAIClient,
|
||||||
projectID: cfg.GoogleTranslate.ProjectID,
|
googleCli: *googleCli,
|
||||||
|
projectID: cfg.GoogleTranslate.ProjectID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||||
return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID)
|
return s.productDescriptionRepo.GetProductDescription(productID, productLangID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates relevant fields with the "updates" map
|
// Updates relevant fields with the "updates" map
|
||||||
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error {
|
||||||
// only some fields can be affected
|
// only some fields can be affected
|
||||||
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
||||||
for key := range updates {
|
for key := range updates {
|
||||||
@@ -105,12 +106,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID)
|
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productLangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates)
|
return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateProductDescription fetches the product description for productFromLangID,
|
// TranslateProductDescription fetches the product description for productFromLangID,
|
||||||
@@ -119,9 +120,9 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
|||||||
//
|
//
|
||||||
// The Google Cloud project must have the Cloud Translation API enabled and the
|
// The Google Cloud project must have the Cloud Translation API enabled and the
|
||||||
// service account must hold the "Cloud Translation API User" role.
|
// service account must hold the "Cloud Translation API User" role.
|
||||||
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
||||||
|
|
||||||
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID)
|
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productFromLangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ package constdata
|
|||||||
|
|
||||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||||
|
const SHOP_ID = 1
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package pagination
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Paging struct {
|
|
||||||
Page uint `json:"page_number" example:"5"`
|
|
||||||
Elements uint `json:"elements_per_page" example:"30"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Paging) Offset() int {
|
|
||||||
return int(p.Elements) * int(p.Page-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Paging) Limit() int {
|
|
||||||
return int(p.Elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Found[T any] struct {
|
|
||||||
Items []T `json:"items,omitempty"`
|
|
||||||
Count uint `json:"items_count" example:"56"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paginate[T any](paging Paging, stmt *gorm.DB) (Found[T], error) {
|
|
||||||
var items []T
|
|
||||||
var count int64
|
|
||||||
|
|
||||||
base := stmt.Session(&gorm.Session{})
|
|
||||||
|
|
||||||
countDB := stmt.Session(&gorm.Session{
|
|
||||||
NewDB: true, // critical: do NOT reuse statement
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := countDB.
|
|
||||||
Table("(?) as sub", base).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return Found[T]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := base.
|
|
||||||
Offset(paging.Offset()).
|
|
||||||
Limit(paging.Limit()).
|
|
||||||
Find(&items).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return Found[T]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Found[T]{
|
|
||||||
Items: items,
|
|
||||||
Count: uint(count),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
150
app/utils/query/filters/filters.go
Normal file
150
app/utils/query/filters/filters.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilterFunction = func(*gorm.DB) *gorm.DB
|
||||||
|
|
||||||
|
func Where(statement string, args ...interface{}) Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where(statement, args...)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StructToWhereScope[T any](model T) Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where(model)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Order(field string, desc bool) Filter {
|
||||||
|
var filt FilterFunction
|
||||||
|
if desc {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Order(field + " DESC")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Order(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: ORDER_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WhereFromStrings(column, conditionOperator, value string) Filter {
|
||||||
|
var filt func(*gorm.DB) *gorm.DB
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "~") {
|
||||||
|
value = strings.ReplaceAll(value, "~", "")
|
||||||
|
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: LIKE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(value, "]") && strings.Contains(value, "[") {
|
||||||
|
period := strings.ReplaceAll(value, "[", "")
|
||||||
|
period = strings.ReplaceAll(period, "]", "")
|
||||||
|
vals := strings.Split(period, ",")
|
||||||
|
if len(vals) == 2 {
|
||||||
|
from, errA := time.Parse("2006-01-02", vals[0])
|
||||||
|
to, errB := time.Parse("2006-01-02", vals[1])
|
||||||
|
if errA == nil && errB == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` BETWEEN ? AND ?`, from.Format("2006-01-02"), to.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` BETWEEN ? AND ?`, vals[0], vals[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conditionOperator == "LIKE" {
|
||||||
|
value = fmt.Sprintf("%%%s%%", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// in future add more grouping functions
|
||||||
|
if strings.Contains(strings.ToLower(column), "count(") {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Having(column+` `+conditionOperator+` ?`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, i)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, f)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b, err := strconv.ParseBool(value); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, b)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/utils/query/filters/filters_list.go
Normal file
107
app/utils/query/filters/filters_list.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use one of declared in the package constants to instantiate the type.
|
||||||
|
type filterCategory = string
|
||||||
|
|
||||||
|
// Enumaration of known types of filters. The assumption is that all filters
|
||||||
|
// belonging to a single category (type) can be used together at a particular
|
||||||
|
// step in the query process.
|
||||||
|
const (
|
||||||
|
// Should be safe to use at any step of longer query series to reduce the
|
||||||
|
// number of results. If it is not, choose a different filter type
|
||||||
|
WHERE_FILTER filterCategory = "where"
|
||||||
|
|
||||||
|
// An like filter
|
||||||
|
LIKE_FILTER filterCategory = "where"
|
||||||
|
|
||||||
|
// An order by clause which can be used at any final step of a complex query
|
||||||
|
// to change the order of results.
|
||||||
|
ORDER_FILTER filterCategory = "order"
|
||||||
|
// TODO: document the special case of filters on products
|
||||||
|
FEAT_VAL_PRODUCT_FILTER filterCategory = "featval_product"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
category filterCategory
|
||||||
|
filter func(*gorm.DB) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) Filter {
|
||||||
|
return Filter{
|
||||||
|
category: category,
|
||||||
|
filter: filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FiltersList struct {
|
||||||
|
filters []Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFiltersList() FiltersList {
|
||||||
|
return FiltersList{
|
||||||
|
// we allocate some extra space beforehand to reduce the overhead of resizing
|
||||||
|
filters: make([]Filter, 0, 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListWithFilter(filt Filter) FiltersList {
|
||||||
|
l := NewFiltersList()
|
||||||
|
l.filters = append(l.filters, filt)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) {
|
||||||
|
f.filters = append(f.filters, NewFilter(category, filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Append(filter ...Filter) {
|
||||||
|
f.filters = append(f.filters, filter...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all stored filters as []func(*gorm.DB)*gorm.DB
|
||||||
|
func (f *FiltersList) All() []func(*gorm.DB) *gorm.DB {
|
||||||
|
return lo.Map(f.filters, func(filt Filter, _ int) func(*gorm.DB) *gorm.DB {
|
||||||
|
return filt.filter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) OfCategory(cat filterCategory) []func(*gorm.DB) *gorm.DB {
|
||||||
|
return lo.Map(lo.Filter(f.filters, func(v Filter, _ int) bool {
|
||||||
|
return v.category == cat
|
||||||
|
}), func(el Filter, _ int) func(*gorm.DB) *gorm.DB {
|
||||||
|
return el.filter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) ApplyAll(d *gorm.DB) {
|
||||||
|
d.Scopes(f.All()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Apply(d *gorm.DB, cat filterCategory) {
|
||||||
|
d.Scopes(f.OfCategory(cat)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Merge(another FiltersList) {
|
||||||
|
f.filters = append(f.filters, another.filters...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An implementation of stringer on FiltersList that is meant rather to be used
|
||||||
|
// for debug display
|
||||||
|
func (f FiltersList) String() string {
|
||||||
|
groupMap := lo.GroupBy(f.filters, func(t Filter) string {
|
||||||
|
return t.category
|
||||||
|
})
|
||||||
|
res := "FiltersList{"
|
||||||
|
for key := range groupMap {
|
||||||
|
res += fmt.Sprintf(" \"%s\": %d filters", key, len(groupMap[key]))
|
||||||
|
}
|
||||||
|
res += " }"
|
||||||
|
return res
|
||||||
|
}
|
||||||
159
app/utils/query/find/find.go
Normal file
159
app/utils/query/find/find.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package find
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Paging struct {
|
||||||
|
Page uint `json:"page_number" example:"5"`
|
||||||
|
Elements uint `json:"elements_per_page" example:"30"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Paging) Offset() int {
|
||||||
|
return int(p.Elements) * int(p.Page-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Paging) Limit() int {
|
||||||
|
return int(p.Elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Found[T any] struct {
|
||||||
|
Items []T `json:"items,omitempty"`
|
||||||
|
Count uint `json:"items_count" example:"56"`
|
||||||
|
Spec map[string]interface{} `json:"spec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps given query adding limit, offset clauses and SQL_CALC_FOUND_ROWS to it
|
||||||
|
// and running SELECT FOUND_ROWS() afterwards to fetch the total number
|
||||||
|
// (ignoring LIMIT) of results. The final results are wrapped into the
|
||||||
|
// [find.Found] type.
|
||||||
|
func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error) {
|
||||||
|
var items []T
|
||||||
|
var count uint64
|
||||||
|
|
||||||
|
// stmt.Debug()
|
||||||
|
|
||||||
|
err := stmt.
|
||||||
|
Clauses(SqlCalcFound()).
|
||||||
|
Offset(paging.Offset()).
|
||||||
|
Limit(paging.Limit()).
|
||||||
|
Find(&items).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return Found[T]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
countInterface, ok := stmt.Get(FOUND_ROWS_CTX_KEY)
|
||||||
|
if !ok {
|
||||||
|
return Found[T]{}, errors.New(FOUND_ROWS_CTX_KEY + " value was not found in the gorm db context")
|
||||||
|
}
|
||||||
|
if count, ok = countInterface.(uint64); !ok {
|
||||||
|
return Found[T]{}, errors.New("failed to cast value under " + FOUND_ROWS_CTX_KEY + " to uint64")
|
||||||
|
}
|
||||||
|
|
||||||
|
columnsSpec := GetColumnsSpec[T](langID)
|
||||||
|
|
||||||
|
return Found[T]{
|
||||||
|
Items: items,
|
||||||
|
Count: uint(count),
|
||||||
|
Spec: map[string]interface{}{
|
||||||
|
"columns": columnsSpec,
|
||||||
|
},
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnsSpec[T any] generates a column specification map for a given struct type T.
|
||||||
|
// Each key is the JSON property name, and the value is a map containing:
|
||||||
|
// - "filter_type": suggested filter type based on field type or `filt` tag
|
||||||
|
// - To disable filtering for a field, set `filt:"none"` in the struct tag
|
||||||
|
// - "sortable": currently hardcoded to true
|
||||||
|
// - "order": order of fields as they appear
|
||||||
|
//
|
||||||
|
// Returns nil if T is not a struct.
|
||||||
|
func GetColumnsSpec[T any](langID uint) map[string]map[string]interface{} {
|
||||||
|
result := make(map[string]map[string]interface{})
|
||||||
|
typ := reflect.TypeOf((*T)(nil)).Elem()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
order := 1
|
||||||
|
processStructFields(langID, typ, result, &order)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FilterTypeRange FilterType = "range"
|
||||||
|
FilterTypeTimerange FilterType = "timerange"
|
||||||
|
FilterTypeLike FilterType = "like"
|
||||||
|
FilterTypeSwitch FilterType = "switch"
|
||||||
|
FilterTypeNone FilterType = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isValidFilterType(ft string) bool {
|
||||||
|
switch FilterType(ft) {
|
||||||
|
case FilterTypeRange, FilterTypeTimerange, FilterTypeLike, FilterTypeSwitch:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processStructFields recursively processes struct fields to populate the result map.
|
||||||
|
// It handles inline structs, reads `json` and `filt` tags, and determines filter types
|
||||||
|
// based on the field type when `filt` tag is absent.
|
||||||
|
// `order` is incremented for each field to track field ordering.
|
||||||
|
func processStructFields(langID uint, typ reflect.Type, result map[string]map[string]interface{}, order *int) {
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
propName := strings.Split(jsonTag, ",")[0]
|
||||||
|
if propName == "" {
|
||||||
|
propName = field.Name
|
||||||
|
}
|
||||||
|
if strings.Contains(jsonTag, ",inline") && field.Type.Kind() == reflect.Struct {
|
||||||
|
processStructFields(langID, field.Type, result, order)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filterType := field.Tag.Get("filt")
|
||||||
|
if filterType != "" {
|
||||||
|
if !isValidFilterType(filterType) {
|
||||||
|
filterType = string(FilterTypeNone)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldType := field.Type.String()
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(fieldType, "int"), strings.HasPrefix(fieldType, "uint"), strings.HasPrefix(fieldType, "float"), strings.HasPrefix(fieldType, "decimal.Decimal"):
|
||||||
|
filterType = string(FilterTypeRange)
|
||||||
|
case strings.Contains(fieldType, "Time"):
|
||||||
|
filterType = string(FilterTypeTimerange)
|
||||||
|
case fieldType == "string":
|
||||||
|
filterType = string(FilterTypeLike)
|
||||||
|
case fieldType == "bool":
|
||||||
|
filterType = string(FilterTypeSwitch)
|
||||||
|
default:
|
||||||
|
filterType = string(FilterTypeNone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result[propName] = map[string]interface{}{
|
||||||
|
"filter_type": filterType,
|
||||||
|
"sortable": func() bool { val, ok := field.Tag.Lookup("sortable"); return !ok || val == "true" }(),
|
||||||
|
"order": *order,
|
||||||
|
"title": i18n.T___(langID, field.Tag.Get("title")),
|
||||||
|
"display": func() bool { val, ok := field.Tag.Lookup("display"); return !ok || val == "true" }(),
|
||||||
|
"hidden": field.Tag.Get("hidden") == "true",
|
||||||
|
}
|
||||||
|
*order++
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/utils/query/find/found_rows_callback.go
Normal file
46
app/utils/query/find/found_rows_callback.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package find
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Key under which result of `SELECT FOUND_ROWS()` should be stored in the
|
||||||
|
// driver context.
|
||||||
|
FOUND_ROWS_CTX_KEY = "maal:found_rows"
|
||||||
|
// Suggested name under which [find.FoundRowsCallback] can be registered.
|
||||||
|
FOUND_ROWS_CALLBACK = "maal:found_rows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Searches query clauses for presence of `SQL_CALC_FOUND_ROWS` and runs `SELECT
|
||||||
|
// FOUND_ROWS();` right after the query containing such clause. The result is
|
||||||
|
// put in the driver context under key [find.FOUND_ROWS_CTX_KEY]. For the
|
||||||
|
// callback to work correctly it must be registered and executed before the
|
||||||
|
// `gorm:preload` callback.
|
||||||
|
func FoundRowsCallback(d *gorm.DB) {
|
||||||
|
if _, ok := d.Statement.Clauses["SELECT"].AfterNameExpression.(sqlCalcFound); ok {
|
||||||
|
var count uint64
|
||||||
|
sqlDB, err := d.DB()
|
||||||
|
if err != nil {
|
||||||
|
_ = d.AddError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := sqlDB.QueryRowContext(d.Statement.Context, "SELECT FOUND_ROWS();")
|
||||||
|
if res == nil {
|
||||||
|
_ = d.AddError(errors.New(`fialed to issue SELECT FOUND_ROWS() query`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.Err() != nil {
|
||||||
|
_ = d.AddError(res.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = res.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
_ = d.AddError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Set(FOUND_ROWS_CTX_KEY, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/utils/query/find/sql_calc_rows.go
Normal file
51
app/utils/query/find/sql_calc_rows.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package find
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlCalcFound struct{}
|
||||||
|
|
||||||
|
// Creates a new Clause which adds `SQL_CALC_FOUND_ROWS` right after `SELECT`.
|
||||||
|
// If [find.FoundRowsCallback] is registered the presence of this clause will
|
||||||
|
// cause `FOUND_ROWS()` result to be available in the driver context.
|
||||||
|
func SqlCalcFound() sqlCalcFound {
|
||||||
|
return sqlCalcFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) Name() string {
|
||||||
|
return "SQL_CALC_FOUND_ROWS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) Build(builder clause.Builder) {
|
||||||
|
_, _ = builder.WriteString("SQL_CALC_FOUND_ROWS")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) MergeClause(cl *clause.Clause) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements [gorm.StatementModifier]
|
||||||
|
func (calc sqlCalcFound) ModifyStatement(stmt *gorm.Statement) {
|
||||||
|
selectClause := stmt.Clauses["SELECT"]
|
||||||
|
if selectClause.AfterNameExpression == nil {
|
||||||
|
selectClause.AfterNameExpression = calc
|
||||||
|
} else if _, ok := selectClause.AfterNameExpression.(sqlCalcFound); !ok {
|
||||||
|
selectClause.AfterNameExpression = exprs{selectClause.AfterNameExpression, calc}
|
||||||
|
}
|
||||||
|
stmt.Clauses["SELECT"] = selectClause
|
||||||
|
}
|
||||||
|
|
||||||
|
type exprs []clause.Expression
|
||||||
|
|
||||||
|
func (exprs exprs) Build(builder clause.Builder) {
|
||||||
|
for idx, expr := range exprs {
|
||||||
|
if idx > 0 {
|
||||||
|
_ = builder.WriteByte(' ')
|
||||||
|
}
|
||||||
|
expr.Build(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/utils/query/query_params/key_mapping.go
Normal file
43
app/utils/query/query_params/key_mapping.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package query_params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
mreflect "git.ma-al.com/goc_daniel/b2b/app/utils/reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MapParamsKeyToDbColumn will attempt to map provided key into unique (prefixed
|
||||||
|
// with table) column name. It will do so using following priority of sources of
|
||||||
|
// mapping:
|
||||||
|
// 1. `formColumnMapping` argument. If the mapped values contain a dot, the part
|
||||||
|
// before the dot will be used for the table name. Otherwise the table name will
|
||||||
|
// be derived from the generic parameter `T`.
|
||||||
|
// 2. json tags of provided as generic `T` struct. The table name will be also
|
||||||
|
// derived from the generic if not provided as dot prefix.
|
||||||
|
func MapParamsKeyToDbColumn[DEFAULT_TABLE_MODEL any](key string, mapping ...map[string]string) (string, error) {
|
||||||
|
ERR := "Failed to find appropiate mapping from form field to database column for key: '%s', and default table name: '%s'"
|
||||||
|
|
||||||
|
if len(mapping) > 0 {
|
||||||
|
if field, ok := (mapping[0])[key]; ok {
|
||||||
|
return field, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var t DEFAULT_TABLE_MODEL
|
||||||
|
if table, field, ok := strings.Cut(key, "."); ok {
|
||||||
|
if column, err := mreflect.GetGormColumnFromJsonField(field, reflect.TypeOf(t)); err == nil {
|
||||||
|
return table + "." + column, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf(ERR, key, table)
|
||||||
|
} else {
|
||||||
|
table := mreflect.GetTableName[DEFAULT_TABLE_MODEL]()
|
||||||
|
if column, err := mreflect.GetGormColumnFromJsonField(key, reflect.TypeOf(t)); err == nil {
|
||||||
|
return table + "." + column, nil
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf(ERR, key, table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf(ERR, key, mreflect.GetTableName[DEFAULT_TABLE_MODEL]())
|
||||||
|
}
|
||||||
63
app/utils/query/query_params/params_query.go
Normal file
63
app/utils/query/query_params/params_query.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package query_params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FunctionalQueryParams = []string{
|
||||||
|
// Used to specidy order of results
|
||||||
|
"sort",
|
||||||
|
// Used to specify page of search resulst
|
||||||
|
"p",
|
||||||
|
// Used to specify number of elements on a page
|
||||||
|
"elems",
|
||||||
|
// Used to specify allowed values of features on products
|
||||||
|
"values",
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFilters[T any](c fiber.Ctx, formColumnMappimg ...map[string]string) (find.Paging, *filters.FiltersList, error) {
|
||||||
|
// field/column based filters
|
||||||
|
filters, err := ParseFieldFilters[T](c, formColumnMappimg...)
|
||||||
|
if err != nil {
|
||||||
|
return find.Paging{}, filters, err
|
||||||
|
}
|
||||||
|
// pagination
|
||||||
|
pageNum, pageSize := ParsePagination(c)
|
||||||
|
|
||||||
|
// ret
|
||||||
|
return find.Paging{Page: pageNum, Elements: pageSize}, filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse field related filters from params query. Produces where clauses and
|
||||||
|
// order rules.
|
||||||
|
func ParseFieldFilters[T any](c fiber.Ctx, formColumnMapping ...map[string]string) (*filters.FiltersList, error) {
|
||||||
|
// var model T
|
||||||
|
list := filters.NewFiltersList()
|
||||||
|
|
||||||
|
whereScopefilters := ParseWhereScopes[T](c, []string{}, formColumnMapping...)
|
||||||
|
list.Append(whereScopefilters...)
|
||||||
|
|
||||||
|
ord, err := ParseOrdering[T](c, formColumnMapping...)
|
||||||
|
if err != nil {
|
||||||
|
return &list, err
|
||||||
|
}
|
||||||
|
// addDefaultOrderingIfNeeded(&ord, model)
|
||||||
|
for i := range ord {
|
||||||
|
if err == nil {
|
||||||
|
list.Append(filters.Order(ord[i].Column, ord[i].IsDesc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add some source of defaults for pagination size here
|
||||||
|
func ParsePagination(c fiber.Ctx) (uint, uint) {
|
||||||
|
pageNum, _ := strconv.ParseInt(c.Query("p", "1"), 10, 64)
|
||||||
|
pageSize, _ := strconv.ParseInt(c.Query("elems", "30"), 10, 64)
|
||||||
|
return uint(pageNum), uint(pageSize)
|
||||||
|
}
|
||||||
82
app/utils/query/query_params/parse_sort.go
Normal file
82
app/utils/query/query_params/parse_sort.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package query_params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ordering struct {
|
||||||
|
Column string
|
||||||
|
IsDesc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseOrdering[T any](c fiber.Ctx, columnMapping ...map[string]string) ([]Ordering, error) {
|
||||||
|
param := c.Query("sort")
|
||||||
|
if len(param) < 1 {
|
||||||
|
return []Ordering{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := strings.Split(param, ";")
|
||||||
|
var orderings []Ordering
|
||||||
|
for _, r := range rules {
|
||||||
|
ord, err := parseOrderingRule[T](r, columnMapping...)
|
||||||
|
if err != nil {
|
||||||
|
return orderings, err
|
||||||
|
}
|
||||||
|
orderings = append(orderings, ord)
|
||||||
|
}
|
||||||
|
return orderings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOrderingRule[T any](rule string, columnMapping ...map[string]string) (Ordering, error) {
|
||||||
|
var desc bool
|
||||||
|
if key, descStr, ok := strings.Cut(rule, ","); ok {
|
||||||
|
switch {
|
||||||
|
case strings.Compare(descStr, "desc") == 0:
|
||||||
|
desc = true
|
||||||
|
case strings.Compare(descStr, "asc") == 0:
|
||||||
|
desc = false
|
||||||
|
default:
|
||||||
|
desc = true
|
||||||
|
}
|
||||||
|
if col, err := MapParamsKeyToDbColumn[T](key, columnMapping...); err == nil {
|
||||||
|
return Ordering{
|
||||||
|
Column: col,
|
||||||
|
IsDesc: desc,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
return Ordering{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if col, err := MapParamsKeyToDbColumn[T](key, columnMapping...); err == nil {
|
||||||
|
return Ordering{
|
||||||
|
Column: col,
|
||||||
|
IsDesc: true,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
return Ordering{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func addDefaultOrderingIfNeeded[T any](previousOrderings *[]Ordering, model T) {
|
||||||
|
// newOrderings := new([]Ordering)
|
||||||
|
// var t T
|
||||||
|
// if len(*previousOrderings) < 1 {
|
||||||
|
// if col, err := mreflect.GetGormColumnFromJsonField("id", reflect.TypeOf(t)); err == nil {
|
||||||
|
// *newOrderings = append(*newOrderings, Ordering{
|
||||||
|
// Column: mreflect.GetTableName[T]() + "." + col,
|
||||||
|
// IsDesc: true,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// if col, err := mreflect.GetGormColumnFromJsonField("iso_code", reflect.TypeOf(t)); err == nil {
|
||||||
|
// *newOrderings = append(*newOrderings, Ordering{
|
||||||
|
// Column: mreflect.GetTableName[T]() + "." + col,
|
||||||
|
// IsDesc: false,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// *newOrderings = append(*newOrderings, *previousOrderings...)
|
||||||
|
// *previousOrderings = *newOrderings
|
||||||
|
// }
|
||||||
|
// }
|
||||||
75
app/utils/query/query_params/where_scope_from_query.go
Normal file
75
app/utils/query/query_params/where_scope_from_query.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package query_params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseWhereScopes will attempt to create where scope query filters from url
|
||||||
|
// query params. It will map form fields to a database column name using
|
||||||
|
// `MapParamsKeyToDbColumn` function.
|
||||||
|
func ParseWhereScopes[T any](c fiber.Ctx, ignoredKeys []string, formColumnMapping ...map[string]string) []filters.Filter {
|
||||||
|
var parsedFilters []filters.Filter
|
||||||
|
//nolint
|
||||||
|
for key, value := range c.Request().URI().QueryArgs().All() {
|
||||||
|
keyStr := string(key)
|
||||||
|
valStr := string(value)
|
||||||
|
|
||||||
|
isIgnored := false
|
||||||
|
for _, ignoredKey := range ignoredKeys {
|
||||||
|
if keyStr == ignoredKey {
|
||||||
|
isIgnored = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isIgnored {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
baseKey, operator := extractOperator(keyStr)
|
||||||
|
|
||||||
|
if col, err := MapParamsKeyToDbColumn[T](baseKey, formColumnMapping...); err == nil {
|
||||||
|
if strings.HasPrefix(valStr, "~") {
|
||||||
|
parsedFilters = append(parsedFilters, filters.WhereFromStrings(col, "LIKE", valStr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
op := resolveOperator(operator)
|
||||||
|
|
||||||
|
parsedFilters = append(parsedFilters, filters.WhereFromStrings(col, op, valStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOperator(key string) (base string, operatorSuffix string) {
|
||||||
|
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq"}
|
||||||
|
for _, suf := range suffixes {
|
||||||
|
if strings.HasSuffix(key, suf) {
|
||||||
|
return strings.TrimSuffix(key, suf), suf[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveOperator(suffix string) string {
|
||||||
|
switch suffix {
|
||||||
|
case "gt":
|
||||||
|
return ">"
|
||||||
|
case "gte":
|
||||||
|
return ">="
|
||||||
|
case "lt":
|
||||||
|
return "<"
|
||||||
|
case "lte":
|
||||||
|
return "<="
|
||||||
|
case "neq":
|
||||||
|
return "!="
|
||||||
|
case "eq":
|
||||||
|
return "="
|
||||||
|
default:
|
||||||
|
return "LIKE"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/utils/query/queryparser/queryparser.go
Normal file
37
app/utils/query/queryparser/queryparser.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package queryparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseQuery(c fiber.Ctx) map[string]interface{} {
|
||||||
|
queryParams := map[string]interface{}{}
|
||||||
|
re := regexp.MustCompile(`\?(\w.+)$`)
|
||||||
|
xx := re.FindAllStringSubmatch(c.Request().URI().String(), -1)
|
||||||
|
|
||||||
|
if len(xx) > 0 {
|
||||||
|
if len(xx[0]) == 2 {
|
||||||
|
queryParts := strings.Split(xx[0][1], "&")
|
||||||
|
for _, q := range queryParts {
|
||||||
|
qq := strings.Split(q, "=")
|
||||||
|
if len(qq) == 2 {
|
||||||
|
if num, err := strconv.ParseInt(qq[1], 10, 64); err == nil {
|
||||||
|
queryParams[qq[0]] = num
|
||||||
|
} else if float, err := strconv.ParseFloat(qq[1], 64); err == nil {
|
||||||
|
queryParams[qq[0]] = float
|
||||||
|
} else {
|
||||||
|
queryParams[qq[0]] = qq[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queryParams[qq[0]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParams
|
||||||
|
}
|
||||||
90
app/utils/reflect/reflect.go
Normal file
90
app/utils/reflect/reflect.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package reflect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: instead of matching with string.Contains use something less error-prone
|
||||||
|
func checkIfContainsJSON(i int, t reflect.Type, name string) string {
|
||||||
|
if wholeTag, ok := t.Field(i).Tag.Lookup("json"); ok {
|
||||||
|
tags := strings.Split(wholeTag, ",")
|
||||||
|
for _, tag := range tags {
|
||||||
|
if name == strings.TrimSpace(tag) {
|
||||||
|
return db.DB.NamingStrategy.ColumnName(t.Name(), t.Field(i).Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not tail recursive but should do fine. Goes only as deep as the hierarchy of
|
||||||
|
// inlined structs.
|
||||||
|
// TODO: improve used internally checkIfContainsJSON
|
||||||
|
func GetGormColumnFromJsonField(jsonName string, t reflect.Type) (string, error) {
|
||||||
|
var res string
|
||||||
|
for i := range make([]bool, t.NumField()) {
|
||||||
|
if tag, ok := t.Field(i).Tag.Lookup("json"); ok && strings.Contains(tag, "inline") {
|
||||||
|
var err error
|
||||||
|
res, err = GetGormColumnFromJsonField(jsonName, t.Field(i).Type)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("no field of struct %q has a name %q in its json form", t.Name(), jsonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res = checkIfContainsJSON(i, t, jsonName)
|
||||||
|
}
|
||||||
|
if res != "" {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no field of struct %q has a name %q in its json form", t.Name(), jsonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTableName[T any]() string {
|
||||||
|
var model T
|
||||||
|
typ := reflect.TypeOf(model).Name()
|
||||||
|
return db.DB.NamingStrategy.TableName(typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetParamFromFieldTag[T any](object T, fieldname string, tagname string, paramname string) string {
|
||||||
|
if table, ok := reflect.TypeOf(object).FieldByName(fieldname); ok {
|
||||||
|
if t, ok := table.Tag.Lookup(tagname); ok {
|
||||||
|
if paramname == "" {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`(?m)` + paramname + `:(\w*)`)
|
||||||
|
f := re.FindAllStringSubmatch(t, -1)
|
||||||
|
if len(re.FindAllStringSubmatch(t, -1)) > 0 {
|
||||||
|
return f[0][1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPrimaryKey[T any](item T) string {
|
||||||
|
var search func(T) string = func(item T) string {
|
||||||
|
val := reflect.ValueOf(item)
|
||||||
|
typ := reflect.TypeOf(item)
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
if gg, ok := typ.Field(i).Tag.Lookup("gorm"); ok {
|
||||||
|
xx := strings.Split(gg, ";")
|
||||||
|
for _, t := range xx {
|
||||||
|
if strings.HasPrefix(strings.ToLower(t), "primarykey") {
|
||||||
|
return db.DB.NamingStrategy.TableName(typ.Field(i).Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val.Field(i).Type().String() == "db.Model" {
|
||||||
|
return db.DB.NamingStrategy.TableName("ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return search(item)
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ var (
|
|||||||
ErrEmailRequired = errors.New("email is required")
|
ErrEmailRequired = errors.New("email is required")
|
||||||
ErrEmailPasswordRequired = errors.New("email and password are required")
|
ErrEmailPasswordRequired = errors.New("email and password are required")
|
||||||
ErrRefreshTokenRequired = errors.New("refresh token is required")
|
ErrRefreshTokenRequired = errors.New("refresh token is required")
|
||||||
|
ErrBadLangID = errors.New("bad language id")
|
||||||
|
ErrBadCountryID = errors.New("bad country id")
|
||||||
|
|
||||||
// Typed errors for password reset
|
// Typed errors for password reset
|
||||||
ErrInvalidResetToken = errors.New("invalid reset token")
|
ErrInvalidResetToken = errors.New("invalid reset token")
|
||||||
@@ -43,6 +45,12 @@ var (
|
|||||||
ErrInvalidXHTML = errors.New("text is not in xhtml format")
|
ErrInvalidXHTML = errors.New("text is not in xhtml format")
|
||||||
ErrAIResponseFail = errors.New("AI responded with failure")
|
ErrAIResponseFail = errors.New("AI responded with failure")
|
||||||
ErrAIBadOutput = errors.New("AI response does not obey the format")
|
ErrAIBadOutput = errors.New("AI response does not obey the format")
|
||||||
|
|
||||||
|
// 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
|
// Error represents an error with HTTP status code
|
||||||
@@ -95,6 +103,10 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
return i18n.T_(c, "error.err_token_required")
|
return i18n.T_(c, "error.err_token_required")
|
||||||
case errors.Is(err, ErrRefreshTokenRequired):
|
case errors.Is(err, ErrRefreshTokenRequired):
|
||||||
return i18n.T_(c, "error.err_refresh_token_required")
|
return i18n.T_(c, "error.err_refresh_token_required")
|
||||||
|
case errors.Is(err, ErrBadLangID):
|
||||||
|
return i18n.T_(c, "error.err_bad_lang_id")
|
||||||
|
case errors.Is(err, ErrBadCountryID):
|
||||||
|
return i18n.T_(c, "error.err_bad_country_id")
|
||||||
|
|
||||||
case errors.Is(err, ErrInvalidResetToken):
|
case errors.Is(err, ErrInvalidResetToken):
|
||||||
return i18n.T_(c, "error.err_invalid_reset_token")
|
return i18n.T_(c, "error.err_invalid_reset_token")
|
||||||
@@ -123,6 +135,12 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrAIBadOutput):
|
case errors.Is(err, ErrAIBadOutput):
|
||||||
return i18n.T_(c, "error.err_ai_bad_output")
|
return i18n.T_(c, "error.err_ai_bad_output")
|
||||||
|
|
||||||
|
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:
|
default:
|
||||||
return i18n.T_(c, "error.err_internal_server_error")
|
return i18n.T_(c, "error.err_internal_server_error")
|
||||||
}
|
}
|
||||||
@@ -145,6 +163,8 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrEmailPasswordRequired),
|
errors.Is(err, ErrEmailPasswordRequired),
|
||||||
errors.Is(err, ErrTokenRequired),
|
errors.Is(err, ErrTokenRequired),
|
||||||
errors.Is(err, ErrRefreshTokenRequired),
|
errors.Is(err, ErrRefreshTokenRequired),
|
||||||
|
errors.Is(err, ErrBadLangID),
|
||||||
|
errors.Is(err, ErrBadCountryID),
|
||||||
errors.Is(err, ErrPasswordsDoNotMatch),
|
errors.Is(err, ErrPasswordsDoNotMatch),
|
||||||
errors.Is(err, ErrTokenPasswordRequired),
|
errors.Is(err, ErrTokenPasswordRequired),
|
||||||
errors.Is(err, ErrInvalidResetToken),
|
errors.Is(err, ErrInvalidResetToken),
|
||||||
@@ -154,7 +174,9 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrInvalidPassword),
|
errors.Is(err, ErrInvalidPassword),
|
||||||
errors.Is(err, ErrBadAttribute),
|
errors.Is(err, ErrBadAttribute),
|
||||||
errors.Is(err, ErrBadField),
|
errors.Is(err, ErrBadField),
|
||||||
errors.Is(err, ErrInvalidXHTML):
|
errors.Is(err, ErrInvalidXHTML),
|
||||||
|
errors.Is(err, ErrBadPaging),
|
||||||
|
errors.Is(err, ErrNoRootFound):
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
|
|||||||
3
bo/components.d.ts
vendored
3
bo/components.d.ts
vendored
@@ -21,12 +21,10 @@ declare module 'vue' {
|
|||||||
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||||
ProductCardFull: typeof import('./src/components/customer/ProductCardFull.vue')['default']
|
|
||||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||||
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
||||||
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
|
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
|
||||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||||
'ProductСustomization': typeof import('./src/components/customer/components/ProductСustomization.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default']
|
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default']
|
||||||
@@ -43,6 +41,7 @@ declare module 'vue' {
|
|||||||
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||||
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
||||||
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||||
|
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
|
||||||
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
||||||
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||||
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const authStore = useAuthStore()
|
|||||||
<RouterLink :to="{ name: 'addresses' }">
|
<RouterLink :to="{ name: 'addresses' }">
|
||||||
Addresses
|
Addresses
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'cart' }">
|
||||||
|
Cart
|
||||||
|
</RouterLink>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProductsView from '@/views/customer/ProductsView.vue';
|
|
||||||
import LangSwitch from './inner/langSwitch.vue'
|
import LangSwitch from './inner/langSwitch.vue'
|
||||||
import ThemeSwitch from './inner/themeSwitch.vue'
|
import ThemeSwitch from './inner/themeSwitch.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|||||||
@@ -134,7 +134,12 @@ function openCreateModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(address: any) {
|
function openEditModal(address: any) {
|
||||||
formData.value = { ...address }
|
formData.value = {
|
||||||
|
street: address.street,
|
||||||
|
zipCode: address.zipCode,
|
||||||
|
city: address.city,
|
||||||
|
country: address.country
|
||||||
|
}
|
||||||
isEditing.value = true
|
isEditing.value = true
|
||||||
editingAddressId.value = address.id
|
editingAddressId.value = address.id
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
|
|||||||
@@ -1,9 +1,218 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto mt-20 px-4 py-8">
|
||||||
|
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
|
||||||
|
<div class="flex flex-col lg:flex-row gap-8 mb-8">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
|
{{ t('Selected Products') }}
|
||||||
|
</h2>
|
||||||
|
<div class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
|
<div class="col-span-4">{{ t('Product') }}</div>
|
||||||
|
<div class="col-span-2 text-right">{{ t('Price') }}</div>
|
||||||
|
<div class="col-span-3 text-center">{{ t('Quantity') }}</div>
|
||||||
|
<div class="col-span-2 text-right">{{ t('Total') }}</div>
|
||||||
|
<div class="col-span-1 text-center">{{ t('Actions') }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="cartStore.items.length > 0">
|
||||||
|
<div v-for="item in cartStore.items" :key="item.id"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-12 gap-4 p-4 border-b border-(--border-light) dark:border-(--border-dark) items-center">
|
||||||
|
<div class="col-span-4 flex items-center gap-4">
|
||||||
|
<div class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||||
|
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||||
|
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<span class="text-black dark:text-white text-sm font-medium">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 text-right">
|
||||||
|
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Price') }}: </span>
|
||||||
|
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3 flex items-center justify-center">
|
||||||
|
<div class="flex items-center border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||||
|
<button @click="decreaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<UIcon name="mdi:minus" />
|
||||||
|
</button>
|
||||||
|
<span class="px-3 py-1 text-black dark:text-white min-w-[40px] text-center">{{ item.quantity }}</span>
|
||||||
|
<button @click="increaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<UIcon name="mdi:plus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 text-right">
|
||||||
|
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Total') }}: </span>
|
||||||
|
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 flex justify-center">
|
||||||
|
<button @click="removeItem(item.id)" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" :title="t('Remove')">
|
||||||
|
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="p-8 text-center">
|
||||||
|
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||||
|
<RouterLink :to="{ name: 'product-card-full' }" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
|
{{ t('Continue Shopping') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-80">
|
||||||
|
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
||||||
|
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
|
||||||
|
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Shipping') }}</span>
|
||||||
|
<span class="text-black dark:text-white">
|
||||||
|
{{ cartStore.shippingCost > 0 ? `$${cartStore.shippingCost.toFixed(2)}` : t('Free') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) }}%)</span>
|
||||||
|
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-6">
|
||||||
|
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
|
||||||
|
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{ cartStore.orderTotal.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
||||||
|
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light) disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{{ t('Place Order') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton block variant="outline" color="neutral" @click="cancelOrder"
|
||||||
|
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||||
|
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||||
|
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||||
|
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||||
|
:class="cartStore.selectedAddressId === address.id
|
||||||
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
|
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||||
|
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-6">
|
||||||
|
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||||
|
<RouterLink :to="{ name: 'addresses' }" class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
|
{{ t('Add Address') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
||||||
|
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||||
|
:class="cartStore.selectedDeliveryMethodId === method.id
|
||||||
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
|
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
|
||||||
|
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-black dark:text-white font-medium">{{ method.name }}</span>
|
||||||
|
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium">
|
||||||
|
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ method.description }}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useCartStore, type CartItem } from '@/stores/cart'
|
||||||
|
import { useAddressStore } from '@/stores/address'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const cartStore = useCartStore()
|
||||||
|
const addressStore = useAddressStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const selectedAddress = ref<number | null>(cartStore.selectedAddressId)
|
||||||
|
const selectedDeliveryMethod = ref<number | null>(cartStore.selectedDeliveryMethodId)
|
||||||
|
const addressSearchQuery = ref('')
|
||||||
|
|
||||||
|
watch(addressSearchQuery, (val) => {
|
||||||
|
addressStore.setSearchQuery(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedAddress, (newValue) => {
|
||||||
|
cartStore.setSelectedAddress(newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedDeliveryMethod, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
cartStore.setDeliveryMethod(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const canPlaceOrder = computed(() => {
|
||||||
|
return cartStore.items.length > 0 &&
|
||||||
|
cartStore.selectedAddressId !== null &&
|
||||||
|
cartStore.selectedDeliveryMethodId !== null
|
||||||
|
})
|
||||||
|
|
||||||
|
function increaseQuantity(item: CartItem) {
|
||||||
|
cartStore.updateQuantity(item.id, item.quantity + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreaseQuantity(item: CartItem) {
|
||||||
|
cartStore.updateQuantity(item.id, item.quantity - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(itemId: number) {
|
||||||
|
cartStore.removeItem(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOrder() {
|
||||||
|
if (canPlaceOrder.value) {
|
||||||
|
console.log('Placing order...')
|
||||||
|
alert(t('Order placed successfully!'))
|
||||||
|
cartStore.clearCart()
|
||||||
|
router.push({ name: 'home' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelOrder() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,14 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TopBar from '@/components/TopBar.vue';
|
import TopBar from '@/components/TopBar.vue';
|
||||||
|
import { getMenu } from '@/router/menu'
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
let menu = await getMenu() as NavigationMenuItem[]
|
||||||
|
|
||||||
|
const openAll = ref(false)
|
||||||
|
|
||||||
|
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||||
|
for (const item of menu) {
|
||||||
|
if(item.children && item.children.length > 0){
|
||||||
|
console.log(item);
|
||||||
|
adaptMenu(item.children);
|
||||||
|
item.open = openAll.value
|
||||||
|
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
|
||||||
|
} else {
|
||||||
|
item.to = { name: 'category', params: item.params };
|
||||||
|
item.icon = 'i-lucide-file-text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = adaptMenu(menu)
|
||||||
|
|
||||||
|
const items = ref<NavigationMenuItem[][]>([
|
||||||
|
[
|
||||||
|
|
||||||
|
...menu as NavigationMenuItem[]
|
||||||
|
],
|
||||||
|
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
<main class="p-10">
|
||||||
<main class="p-10">
|
<TopBar/>
|
||||||
<TopBar/>
|
<div class="mt-24 w-1/4 bg-accented rounded-2xl">
|
||||||
<router-view />
|
<button @click="openAll = !openAll">open all</button>
|
||||||
</main>
|
<UNavigationMenu arrow orientation="vertical" :items="items" class="p-4" :key="openAll">
|
||||||
|
</UNavigationMenu>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<main :key="i18n.locale.value">
|
<main :key="i18n.locale.value">
|
||||||
<TopBarLogin />
|
<TopBarLogin />
|
||||||
<router-view />
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Empty from '@/layouts/empty.vue'
|
|
||||||
import { currentLang, langs } from './langs'
|
import { currentLang, langs } from './langs'
|
||||||
import { getSettings } from './settings'
|
import { getSettings } from './settings'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import Default from '@/layouts/default.vue'
|
import Default from '@/layouts/default.vue'
|
||||||
|
import { getMenu } from './menu'
|
||||||
|
|
||||||
function isAuthenticated(): boolean {
|
function isAuthenticated(): boolean {
|
||||||
if (typeof document === 'undefined') return false
|
if (typeof document === 'undefined') return false
|
||||||
@@ -12,6 +12,7 @@ function isAuthenticated(): boolean {
|
|||||||
|
|
||||||
|
|
||||||
await getSettings()
|
await getSettings()
|
||||||
|
const routes = await getMenu()
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -24,6 +25,7 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/:locale',
|
path: '/:locale',
|
||||||
children: [
|
children: [
|
||||||
|
{ path: 'category/:category_id-:link_rewrite', component: () => import('../views/CategoryView.vue'), name: 'category' },
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: Default,
|
component: Default,
|
||||||
@@ -33,51 +35,91 @@ const router = createRouter({
|
|||||||
{ path: 'products-datail/', component: () => import('../components/admin/ProductDetailView.vue'), name: 'product-detail' },
|
{ path: 'products-datail/', component: () => import('../components/admin/ProductDetailView.vue'), name: 'product-detail' },
|
||||||
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
||||||
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
||||||
|
{ 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: '',
|
path: '/:pathMatch(.*)*',
|
||||||
component: Empty,
|
component: () => import('@/views/NotFoundView.vue'),
|
||||||
children: [
|
name: 'not-found',
|
||||||
{ 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) => {
|
// router.beforeEach((to, from, next) => {
|
||||||
|
// const locale = to.params.locale as string
|
||||||
|
// const localeLang = langs.find((x) => x.iso_code == locale)
|
||||||
|
|
||||||
|
// if (locale && langs.length > 0) {
|
||||||
|
// const authStore = useAuthStore()
|
||||||
|
// // console.log(authStore.isAuthenticated, to, from)
|
||||||
|
// // if()
|
||||||
|
// const validLocale = langs.find((l) => l.lang_code === locale)
|
||||||
|
|
||||||
|
// if (validLocale) {
|
||||||
|
// currentLang.value = localeLang
|
||||||
|
// if (!to.meta?.guest && !isAuthenticated()) {
|
||||||
|
// return next({ name: 'login', params: { locale } })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // return next()
|
||||||
|
// return next()
|
||||||
|
// } else if (locale) {
|
||||||
|
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!locale && to.path !== '/') {
|
||||||
|
// return next(`/${currentLang.value?.iso_code}${to.path}`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// next()
|
||||||
|
// })
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
const locale = to.params.locale as string
|
const locale = to.params.locale as string
|
||||||
const localeLang = langs.find((x) => x.iso_code == locale)
|
const localeLang = langs.find((x) => x.iso_code === locale)
|
||||||
|
|
||||||
if (locale && langs.length > 0) {
|
if (locale && langs.length > 0) {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
console.log(authStore.isAuthenticated, to, from)
|
|
||||||
// if()
|
|
||||||
const validLocale = langs.find((l) => l.lang_code === locale)
|
const validLocale = langs.find((l) => l.lang_code === locale)
|
||||||
|
|
||||||
if (validLocale) {
|
if (validLocale) {
|
||||||
currentLang.value = localeLang
|
currentLang.value = localeLang
|
||||||
if (!to.meta?.guest && !isAuthenticated()) {
|
|
||||||
return next({ name: 'login', params: { locale } })
|
if (!to.meta?.guest && !authStore.isAuthenticated) {
|
||||||
|
return { name: 'login', params: { locale } }
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return true
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!locale && to.path !== '/') {
|
if (!locale && to.path !== '/') {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
return `/${currentLang.value?.iso_code}${to.path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
17
bo/src/router/menu.ts
Normal file
17
bo/src/router/menu.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useFetchJson } from "@/composable/useFetchJson";
|
||||||
|
import type { MenuItem, Route } from "@/types/menu";
|
||||||
|
|
||||||
|
export const getMenu = async () => {
|
||||||
|
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu');
|
||||||
|
|
||||||
|
return resp.items.children
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getRoutes = async () => {
|
||||||
|
const resp = await useFetchJson<Route[]>('/api/v1/restricted/menu/get-routes');
|
||||||
|
|
||||||
|
return resp.items
|
||||||
|
|
||||||
|
}
|
||||||
@@ -88,14 +88,16 @@ export const useAddressStore = defineStore('address', () => {
|
|||||||
const index = addresses.value.findIndex(a => a.id === id)
|
const index = addresses.value.findIndex(a => a.id === id)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
|
const existing = addresses.value[index]
|
||||||
|
if (!existing) return false
|
||||||
|
|
||||||
addresses.value[index] = {
|
addresses.value[index] = {
|
||||||
...addresses.value[index],
|
id: existing.id,
|
||||||
...normalize(formData)
|
...normalize(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAddress(id: number): boolean {
|
function deleteAddress(id: number): boolean {
|
||||||
const index = addresses.value.findIndex(a => a.id === id)
|
const index = addresses.value.findIndex(a => a.id === id)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|||||||
113
bo/src/stores/cart.ts
Normal file
113
bo/src/stores/cart.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export interface CartItem {
|
||||||
|
id: number
|
||||||
|
productId: number
|
||||||
|
name: string
|
||||||
|
image: string
|
||||||
|
price: number
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeliveryMethod {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
price: number
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCartStore = defineStore('cart', () => {
|
||||||
|
const items = ref<CartItem[]>([])
|
||||||
|
const selectedAddressId = ref<number | null>(null)
|
||||||
|
const selectedDeliveryMethodId = ref<number | null>(null)
|
||||||
|
const shippingCost = ref(0)
|
||||||
|
const vatRate = ref(0.23) // 23% VAT
|
||||||
|
|
||||||
|
const deliveryMethods = ref<DeliveryMethod[]>([
|
||||||
|
{ id: 1, name: 'Standard Delivery', price: 0, description: '5-7 business days' },
|
||||||
|
{ id: 2, name: 'Express Delivery', price: 15, description: '2-3 business days' },
|
||||||
|
{ id: 3, name: 'Priority Delivery', price: 30, description: 'Next business day' }
|
||||||
|
])
|
||||||
|
|
||||||
|
function initMockData() {
|
||||||
|
items.value = [
|
||||||
|
{ id: 1, productId: 101, name: 'Premium Widget Pro', image: '/img/product-1.jpg', price: 129.99, quantity: 2 },
|
||||||
|
{ id: 2, productId: 102, name: 'Ultra Gadget X', image: '/img/product-2.jpg', price: 89.50, quantity: 1 },
|
||||||
|
{ id: 3, productId: 103, name: 'Mega Tool Set', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const productsTotal = computed(() => {
|
||||||
|
return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const vatAmount = computed(() => {
|
||||||
|
return productsTotal.value * vatRate.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderTotal = computed(() => {
|
||||||
|
return productsTotal.value + shippingCost.value + vatAmount.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemCount = computed(() => {
|
||||||
|
return items.value.reduce((sum, item) => sum + item.quantity, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateQuantity(itemId: number, quantity: number) {
|
||||||
|
const item = items.value.find(i => i.id === itemId)
|
||||||
|
if (item) {
|
||||||
|
if (quantity <= 0) {
|
||||||
|
removeItem(itemId)
|
||||||
|
} else {
|
||||||
|
item.quantity = quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(itemId: number) {
|
||||||
|
const index = items.value.findIndex(i => i.id === itemId)
|
||||||
|
if (index !== -1) {
|
||||||
|
items.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCart() {
|
||||||
|
items.value = []
|
||||||
|
selectedAddressId.value = null
|
||||||
|
selectedDeliveryMethodId.value = null
|
||||||
|
shippingCost.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedAddress(addressId: number | null) {
|
||||||
|
selectedAddressId.value = addressId
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDeliveryMethod(methodId: number) {
|
||||||
|
selectedDeliveryMethodId.value = methodId
|
||||||
|
const method = deliveryMethods.value.find(m => m.id === methodId)
|
||||||
|
if (method) {
|
||||||
|
shippingCost.value = method.price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initMockData()
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
selectedAddressId,
|
||||||
|
selectedDeliveryMethodId,
|
||||||
|
shippingCost,
|
||||||
|
vatRate,
|
||||||
|
deliveryMethods,
|
||||||
|
productsTotal,
|
||||||
|
vatAmount,
|
||||||
|
orderTotal,
|
||||||
|
itemCount,
|
||||||
|
updateQuantity,
|
||||||
|
removeItem,
|
||||||
|
clearCart,
|
||||||
|
setSelectedAddress,
|
||||||
|
setDeliveryMethod
|
||||||
|
}
|
||||||
|
})
|
||||||
26
bo/src/types/menu.d.ts
vendored
Normal file
26
bo/src/types/menu.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export interface MenuItem {
|
||||||
|
category_id: number
|
||||||
|
label: string
|
||||||
|
params: Params
|
||||||
|
children: MenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Params {
|
||||||
|
category_id?: number
|
||||||
|
link_rewrite?: string
|
||||||
|
locale?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
ID: number
|
||||||
|
Name: string
|
||||||
|
Path: string
|
||||||
|
Component: string
|
||||||
|
Layout: string
|
||||||
|
Meta: string
|
||||||
|
IsActive: boolean
|
||||||
|
SortOrder: number
|
||||||
|
ParentID: any
|
||||||
|
Parent: any
|
||||||
|
Children: any
|
||||||
|
}
|
||||||
21
bo/src/views/CategoryView.vue
Normal file
21
bo/src/views/CategoryView.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mt-24">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Category</h1>
|
||||||
|
{{ $route.params }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// import { useRoute } from 'vue-router';
|
||||||
|
import Default from '@/layouts/default.vue';
|
||||||
|
// const route = useRoute()
|
||||||
|
// console.log(route);
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -52,9 +52,11 @@ const PrivacyComponent = computed(() =>
|
|||||||
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
|
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<component :is="Empty || 'div'">
|
||||||
<UDrawer v-model:open="showTherms" :overlay="false">
|
<UDrawer v-model:open="showTherms" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="TermsComponent" />
|
<component :is="TermsComponent" />
|
||||||
@@ -163,4 +165,5 @@ const PrivacyComponent = computed(() =>
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
7
bo/src/views/NotFoundView.vue
Normal file
7
bo/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center h-screen">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-800">404</h1>
|
||||||
|
<p class="mt-4 text-lg text-gray-600">Page not found</p>
|
||||||
|
<router-link to="/" class="mt-6 text-blue-500 hover:underline">Go back home</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -36,70 +37,74 @@ function validate(): FormError[] {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
<component :is="Empty || 'div'">
|
||||||
<div class="text-center mb-15">
|
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="text-center mb-15">
|
||||||
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">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
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>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
</div>
|
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
|
||||||
|
|
||||||
<template v-if="submitted">
|
<template v-if="submitted">
|
||||||
<div class="text-center flex flex-col gap-4">
|
<div class="text-center flex flex-col gap-4">
|
||||||
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
|
<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>
|
<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">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('general.password_reset_link_sent_notice') }}
|
{{ $t('general.password_reset_link_sent_notice') }}
|
||||||
</p>
|
</p>
|
||||||
<UButton color="neutral" variant="outline" block @click="goToLogin"
|
<UButton color="neutral" variant="outline" block @click="goToLogin"
|
||||||
class="dark:text-white text-black cursor-pointer">
|
class="dark:text-white text-black cursor-pointer">
|
||||||
{{ $t('general.back_to_sign_in') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('general.enter_email_for_password_reset') }}
|
{{ $t('general.enter_email_for_password_reset') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
|
<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"
|
<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' }"
|
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
||||||
@close="authStore.clearError" />
|
@close="authStore.clearError" />
|
||||||
|
|
||||||
<UFormField :label="$t('general.email_address')" name="email" required
|
<UFormField :label="$t('general.email_address')" name="email" required
|
||||||
class="w-full dark:text-white text-black">
|
class="w-full dark:text-white text-black">
|
||||||
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
|
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
|
||||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading"
|
<UButton type="submit" block :loading="authStore.loading"
|
||||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.send_password_reset_link') }}
|
{{ $t('general.send_password_reset_link') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
<div
|
||||||
<button color="neutral" variant="outline" :loading="authStore.loading"
|
class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
||||||
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
<button color="neutral" variant="outline" :loading="authStore.loading"
|
||||||
@click="goToLogin">
|
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
||||||
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
|
@click="goToLogin">
|
||||||
{{ $t('general.back_to_sign_in') }}
|
<UIcon name="mingcute:arrow-left-line"
|
||||||
</button>
|
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
{{ $t('general.back_to_sign_in') }}
|
||||||
{{ $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>
|
</button>
|
||||||
</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
</div>
|
{{ $t('general.dont_have_an_account') }}
|
||||||
</template>
|
<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>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<component :is="Empty || 'div'">
|
||||||
<UDrawer v-model:open="showTherms" :overlay="false">
|
<UDrawer v-model:open="showTherms" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="TermsComponent" />
|
<component :is="TermsComponent" />
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
</UForm>
|
</UForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -120,6 +122,8 @@ import { useValidation } from '@/composable/useValidation'
|
|||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ProductDetailView from './customer/ProductDetailView.vue'
|
|
||||||
import ProductsView from './customer/ProductsView.vue'
|
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -49,80 +50,84 @@ function validate(): FormError[] {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
<component :is="Empty || 'div'">
|
||||||
<div class="text-center mb-15">
|
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="text-center mb-15">
|
||||||
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">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
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">
|
||||||
</div>
|
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||||
<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>
|
</div>
|
||||||
</template>
|
<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-else>
|
<template v-if="submitted">
|
||||||
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
|
<div class="text-center flex flex-col gap-4">
|
||||||
<UAlert v-if="authStore.error" color="error" variant="subtle"
|
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
|
||||||
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
|
<h2 class="text-xl font-semibold dark:text-white text-black">
|
||||||
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
{{ $t('general.password_updated') }}
|
||||||
@close="authStore.clearError" />
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<UFormField :label="$t('general.new_password')" name="new_password" required
|
{{ $t('general.password_updated_description') }}
|
||||||
class="w-full dark:text-white text-black">
|
</p>
|
||||||
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
|
<UButton block @click="goToLogin" class="dark:text-white text-black">
|
||||||
: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') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</button>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</template>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useFetchJson } from '@/composable/useFetchJson'
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const { t, te } = useI18n()
|
const { t, te } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -66,81 +67,84 @@ function goToLogin() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<component :is="Empty || '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
|
||||||
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
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="w-full max-w-md">
|
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-8">
|
<div class="w-full max-w-md">
|
||||||
<div
|
<div class="text-center mb-8">
|
||||||
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">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
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>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
<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>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -11,7 +11,9 @@ require (
|
|||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1
|
||||||
github.com/openai/openai-go/v3 v3.28.0
|
github.com/openai/openai-go/v3 v3.28.0
|
||||||
|
github.com/samber/lo v1.53.0
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
google.golang.org/api v0.247.0
|
google.golang.org/api v0.247.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -109,6 +109,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||||
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
|
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
|
||||||
@@ -124,6 +126,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- create routes table
|
-- create routes table
|
||||||
CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
|
CREATE TABLE IF NOT EXISTS b2b_routes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
path VARCHAR(255) NULL,
|
path VARCHAR(255) NULL,
|
||||||
component VARCHAR(255) NOT NULL COMMENT 'path to component file',
|
component VARCHAR(255) NOT NULL COMMENT 'path to component file',
|
||||||
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
|
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
|
||||||
meta JSON DEFAULT '{}' ,
|
meta JSON DEFAULT '{}',
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
sort_order INT DEFAULT 0,
|
sort_order INT DEFAULT 0,
|
||||||
parent_id INT NULL
|
parent_id INT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT fk_parent
|
||||||
|
FOREIGN KEY (parent_id)
|
||||||
|
REFERENCES b2b_routes(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
ALTER TABLE b2b_tracker_routes
|
INSERT IGNORE INTO b2b_routes
|
||||||
ADD CONSTRAINT fk_parent
|
|
||||||
FOREIGN KEY (parent_id) REFERENCES b2b_tracker_routes(id)
|
|
||||||
ON DELETE SET NULL;
|
|
||||||
|
|
||||||
INSERT IGNORE INTO b2b_tracker_routes
|
|
||||||
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
||||||
VALUES
|
VALUES
|
||||||
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
|
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
|
||||||
@@ -30,5 +30,5 @@ VALUES
|
|||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
||||||
DROP TABLE IF EXISTS b2b_tracker_routes;
|
DROP TABLE IF EXISTS b2b_routes;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ INSERT IGNORE INTO b2b_language
|
|||||||
VALUES
|
VALUES
|
||||||
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
|
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
|
||||||
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
|
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
|
||||||
(3, '2022-09-16 17:10:02.865', '2026-03-02 21:24:36.779730', NULL, 'Čeština', 'cs', 'cs', '__-__-____', '__-__', 0, 0, 1, '🇨🇿'),
|
(3, '2022-09-16 17:10:02.865', '2026-03-02 21:24:36.779730', NULL, 'Čeština', 'cs', 'cs', '__-__-____', '__-__', 0, 0, 0, '🇨🇿'),
|
||||||
(4, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
|
(4, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS b2b_components (
|
CREATE TABLE IF NOT EXISTS b2b_components (
|
||||||
@@ -72,7 +72,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
|
|||||||
password_reset_expires DATETIME(6) NULL,
|
password_reset_expires DATETIME(6) NULL,
|
||||||
last_password_reset_request DATETIME(6) NULL,
|
last_password_reset_request DATETIME(6) NULL,
|
||||||
last_login_at DATETIME(6) NULL,
|
last_login_at DATETIME(6) NULL,
|
||||||
lang VARCHAR(10) NULL DEFAULT 'en',
|
lang_id BIGINT NULL DEFAULT 2,
|
||||||
|
country_id BIGINT NULL DEFAULT 2,
|
||||||
created_at DATETIME(6) NULL,
|
created_at DATETIME(6) NULL,
|
||||||
updated_at DATETIME(6) NULL,
|
updated_at DATETIME(6) NULL,
|
||||||
deleted_at DATETIME(6) NULL
|
deleted_at DATETIME(6) NULL
|
||||||
@@ -112,15 +113,26 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_to
|
|||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
|
||||||
|
|
||||||
|
|
||||||
-- insert sample admin user admin@ma-al.com/Maal12345678
|
-- countries
|
||||||
|
CREATE TABLE IF NOT EXISTS b2b_countries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
currency INT UNSIGNED NOT NULL,
|
||||||
|
flag VARCHAR(16) NOT NULL,
|
||||||
|
CONSTRAINT fk_countries_currency FOREIGN KEY (currency) REFERENCES ps_currency(id_currency) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
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, created_at, updated_at, deleted_at)
|
INSERT IGNORE INTO b2b_countries
|
||||||
|
(id, name, currency, flag)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 'admin', 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 'pl', '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL);
|
(1, 'Polska', 1, '🇵🇱'),
|
||||||
ALTER TABLE b2b_customers AUTO_INCREMENT = 1;
|
(2, 'England', 2, '🇬🇧'),
|
||||||
|
(3, 'Čeština', 2, '🇨🇿'),
|
||||||
|
(4, 'Deutschland', 2, '🇩🇪');
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS b2b_countries;
|
||||||
DROP TABLE IF EXISTS b2b_language;
|
DROP TABLE IF EXISTS b2b_language;
|
||||||
DROP TABLE IF EXISTS b2b_components;
|
DROP TABLE IF EXISTS b2b_components;
|
||||||
DROP TABLE IF EXISTS b2b_scopes;
|
DROP TABLE IF EXISTS b2b_scopes;
|
||||||
|
|||||||
431
package-lock.json
generated
431
package-lock.json
generated
@@ -4,6 +4,7 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "b2b",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "^4.5.1",
|
"@nuxt/ui": "^4.5.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -1006,6 +1007,395 @@
|
|||||||
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
@@ -1975,13 +2365,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@unhead/vue": {
|
"node_modules/@unhead/vue": {
|
||||||
"version": "2.1.10",
|
"version": "2.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.12.tgz",
|
||||||
"integrity": "sha512-VP78Onh2HNezLPfhYjfHqn4dxlcQsE6PJgTTs61NksO/thvilNswtgBq0N0MWCLtn43N5akEPGW2y2zxM3PWgQ==",
|
"integrity": "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hookable": "^6.0.1",
|
"hookable": "^6.0.1",
|
||||||
"unhead": "2.1.10"
|
"unhead": "2.1.12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/harlan-zw"
|
"url": "https://github.com/sponsors/harlan-zw"
|
||||||
@@ -2801,6 +3191,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fuse.js": {
|
"node_modules/fuse.js": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
@@ -2846,9 +3251,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/h3": {
|
"node_modules/h3": {
|
||||||
"version": "1.15.5",
|
"version": "1.15.8",
|
||||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz",
|
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.8.tgz",
|
||||||
"integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==",
|
"integrity": "sha512-iOH6Vl8mGd9nNfu9C0IZ+GuOAfJHcyf3VriQxWaSWIB76Fg4BnFuk4cxBxjmQSSxJS664+pgjP6e7VBnUzFfcg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.2",
|
||||||
@@ -4257,9 +4662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unhead": {
|
"node_modules/unhead": {
|
||||||
"version": "2.1.10",
|
"version": "2.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz",
|
||||||
"integrity": "sha512-We8l9uNF8zz6U8lfQaVG70+R/QBfQx1oPIgXin4BtZnK2IQpz6yazQ0qjMNVBDw2ADgF2ea58BtvSK+XX5AS7g==",
|
"integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hookable": "^6.0.1"
|
"hookable": "^6.0.1"
|
||||||
@@ -4269,9 +4674,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unhead/node_modules/hookable": {
|
"node_modules/unhead/node_modules/hookable": {
|
||||||
"version": "6.0.1",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
|
||||||
"integrity": "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==",
|
"integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unifont": {
|
"node_modules/unifont": {
|
||||||
|
|||||||
Reference in New Issue
Block a user