11 Commits

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

4
.env
View File

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

1
.gitignore vendored
View File

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

2995
ADD_THIS_TO_SQL.sql Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -52,20 +52,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
// "override_currency": c.Query("override_currency", ""), // "override_currency": c.Query("override_currency", ""),
// } // }
id_shop_attribute := c.Query("shopID")
id_shop, err := strconv.Atoi(id_shop_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2")) id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
listing, err := h.listProductsService.GetListing(uint(id_shop), uint(id_lang), paging, filters) listing, err := h.listProductsService.GetListing(uint(id_lang), paging, filters)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,11 +62,11 @@ type Product struct {
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"` DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
} }
type ProductInList struct { type ProductInList struct {
ID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"` ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
Name string `gorm:"column:name;default:'no name'" json:"name" form:"name"` Name string `gorm:"column:name" json:"name" form:"name"`
Price float64 `gorm:"column:price;default:0.0" json:"price" form:"price"` ImageID uint `gorm:"column:id_image"`
ActiveAsProduct uint `gorm:"column:active;default:0" json:"active_as_product" form:"active_as_product"` LinkRewrite string `gorm:"column:link_rewrite"`
ActiveInShop uint `gorm:"column:active;default:0" json:"active_in_shop" form:"active_in_shop"` Active uint `gorm:"column:active" json:"active" form:"active"`
} }
type ProductFilters struct { type ProductFilters struct {
@@ -81,4 +81,28 @@ type ProductFilters struct {
InStock uint `query:"stock,omitempty"` InStock uint `query:"stock,omitempty"`
} }
type ScannedCategory struct {
CategoryID uint `gorm:"column:ID;primaryKey"`
Name string `gorm:"column:name"`
Active uint `gorm:"column:active"`
Position uint `gorm:"column:position"`
ParentID uint `gorm:"column:id_parent"`
IsRoot uint `gorm:"column:is_root_category"`
LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"`
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategpryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategpryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
}
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint

View File

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

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

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

View File

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

View File

@@ -3,12 +3,13 @@ package listProductsRepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
) )
type UIListProductsRepo interface { type UIListProductsRepo interface {
GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
} }
type ListProductsRepo struct{} type ListProductsRepo struct{}
@@ -17,19 +18,10 @@ func New() UIListProductsRepo {
return &ListProductsRepo{} return &ListProductsRepo{}
} }
func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
var listing []model.ProductInList var listing []model.ProductInList
var total int64 var total int64
// Apply filters here
q := db.DB.Table("ps_product").
Select("ps_product.id_product AS id_product", "ps_product_lang.name AS name", "ps_product_shop.price AS price", "ps_product.active AS active_as_product", "ps_product_shop.active AS active_in_shop").
Joins("LEFT JOIN ps_product_shop ON ps_product.id_product = ps_product_shop.id_product").
Joins("LEFT JOIN ps_product_lang ON ps_product.id_product = ps_product_lang.id_product").
Where("ps_product_shop.id_shop = ?", id_shop).
Where("ps_product_lang.id_shop = ?", id_shop).
Where("ps_product_lang.id_lang = ?", id_lang)
// var resultIDs []uint // var resultIDs []uint
// q := db.DB. // q := db.DB.
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and // // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
@@ -44,15 +36,41 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi
// Limit(p.Limit()). // Limit(p.Limit()).
// Offset(p.Offset()) // Offset(p.Offset())
err := q.Count(&total).Error err := db.DB.Raw(`
SELECT
ps_product.id_product AS ID,
ps_product_lang.name AS name,
ps_product.active AS active,
ps_product_lang.link_rewrite AS link_rewrite,
COALESCE (
ps_image_shop.id_image, any_image.id_image
) AS id_image
FROM ps_product
LEFT JOIN ps_product_lang
ON ps_product_lang.id_product = ps_product.id_product
AND ps_product_lang.id_shop = ?
AND ps_product_lang.id_lang = ?
LEFT JOIN ps_image_shop
ON ps_image_shop.id_product = ps_product.id_product
AND ps_image_shop.id_shop = ?
AND ps_image_shop.cover = 1
LEFT JOIN (
SELECT id_product, MIN(id_image) AS id_image
FROM ps_image
GROUP BY id_product
) any_image
ON ps_product.id_product = any_image.id_product
LIMIT ? OFFSET ?`,
constdata.SHOP_ID, id_lang, constdata.SHOP_ID, p.Limit(), p.Offset()).
Scan(&listing).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return find.Found[model.ProductInList]{}, err
} }
err = q. err = db.DB.Raw(`
Limit(p.Limit()). SELECT COUNT(*)
Offset(p.Offset()). FROM ps_product`).
Scan(&listing).Error Scan(&total).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return find.Found[model.ProductInList]{}, err
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
bo/components.d.ts vendored
View File

@@ -19,7 +19,6 @@ declare module 'vue' {
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
PageCart: typeof import('./src/components/customer/PageCart.vue')['default'] PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default'] PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
PageProductsList: typeof import('./src/components/customer/PageProductsList.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']
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default'] ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
@@ -42,6 +41,7 @@ declare module 'vue' {
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default'] UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']

View File

@@ -31,9 +31,6 @@ const authStore = useAuthStore()
<RouterLink :to="{ name: 'cart' }"> <RouterLink :to="{ name: 'cart' }">
Cart Cart
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'products-list' }">
Products List
</RouterLink>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher --> <!-- Language Switcher -->
<LangSwitch /> <LangSwitch />

View File

@@ -14,9 +14,9 @@ const authStore = useAuthStore()
<!-- Logo --> <!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2"> <RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center"> <div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" /> <UIcon name="i-heroicons-clock" class="w-5 h-5" />
</div> </div>
<span class="font-semibold text-gray-900 dark:text-white">B2B</span> <span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink> </RouterLink>
<!-- Right Side Actions --> <!-- Right Side Actions -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="container mx-auto mt-10"> <div class="container mx-auto mt-10">
<div class="flex flex-col gap-5 mb-6"> <div class="flex flex-col mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0"> <div class="flex justify-between items-center">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')" <UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" /> class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
@@ -16,6 +16,7 @@
</UButton> </UButton>
</div> </div>
</div> </div>
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="address in paginatedAddresses" :key="address.id" <div v-for="address in paginatedAddresses" :key="address.id"
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow"> class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow">
@@ -34,9 +35,11 @@
</div> </div>
</div> </div>
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div> <div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
<div class="mt-6 flex justify-center"> <div class="mt-6 flex justify-center">
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" /> <UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
</div> </div>
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto"> <UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
<template #content> <template #content>
<div class="p-6 flex flex-col gap-6"> <div class="p-6 flex flex-col gap-6">
@@ -71,6 +74,7 @@
</div> </div>
</template> </template>
</UModal> </UModal>
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto"> <UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
<template #content> <template #content>
<div class="p-6 flex flex-col gap-3"> <div class="p-6 flex flex-col gap-3">
@@ -102,6 +106,7 @@ import { useI18n } from 'vue-i18n'
const addressStore = useAddressStore() const addressStore = useAddressStore()
const { t } = useI18n() const { t } = useI18n()
const searchQuery = ref('') const searchQuery = ref('')
const showModal = ref(false) const showModal = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
@@ -117,14 +122,17 @@ const totalItems = computed(() => addressStore.totalItems)
const pageSize = addressStore.pageSize const pageSize = addressStore.pageSize
watch(page, (newPage) => addressStore.setPage(newPage)) watch(page, (newPage) => addressStore.setPage(newPage))
watch(searchQuery, (val) => { watch(searchQuery, (val) => {
addressStore.setSearchQuery(val) addressStore.setSearchQuery(val)
}) })
function openCreateModal() { function openCreateModal() {
resetForm() resetForm()
isEditing.value = false isEditing.value = false
showModal.value = true showModal.value = true
} }
function openEditModal(address: any) { function openEditModal(address: any) {
formData.value = { formData.value = {
street: address.street, street: address.street,
@@ -136,14 +144,17 @@ function openEditModal(address: any) {
editingAddressId.value = address.id editingAddressId.value = address.id
showModal.value = true showModal.value = true
} }
function resetForm() { function resetForm() {
formData.value = { street: '', zipCode: '', city: '', country: '' } formData.value = { street: '', zipCode: '', city: '', country: '' }
editingAddressId.value = null editingAddressId.value = null
} }
function closeModal() { function closeModal() {
showModal.value = false showModal.value = false
resetForm() resetForm()
} }
function validate() { function validate() {
const errors = [] const errors = []
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' }) if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
@@ -152,6 +163,7 @@ function validate() {
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' }) if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
return errors.length ? errors : null return errors.length ? errors : null
} }
function saveAddress() { function saveAddress() {
if (validate()) return if (validate()) return
if (isEditing.value && editingAddressId.value) { if (isEditing.value && editingAddressId.value) {
@@ -161,18 +173,12 @@ function saveAddress() {
} }
closeModal() closeModal()
} }
// const Lera = ref('')
// function run (){
// if(Lera.value==='lera'){
// console.log('Leraa okokok')
// }else{
// console.log('LEra nonono')
// }
// }
function confirmDelete(id: number) { function confirmDelete(id: number) {
addressToDelete.value = id addressToDelete.value = id
showDeleteConfirm.value = true showDeleteConfirm.value = true
} }
function deleteAddress() { function deleteAddress() {
if (addressToDelete.value) { if (addressToDelete.value) {
addressStore.deleteAddress(addressToDelete.value) addressStore.deleteAddress(addressToDelete.value)

View File

@@ -3,14 +3,11 @@
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1> <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 flex-col lg:flex-row gap-8 mb-8">
<div class="flex-1"> <div class="flex-1">
<div <div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
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)">
<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') }} {{ t('Selected Products') }}
</h2> </h2>
<div <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)">
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-4">{{ t('Product') }}</div>
<div class="col-span-2 text-right">{{ t('Price') }}</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-3 text-center">{{ t('Quantity') }}</div>
@@ -21,8 +18,7 @@
<div v-for="item in cartStore.items" :key="item.id" <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"> 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="col-span-4 flex items-center gap-4">
<div <div class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
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" /> <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" /> <UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
</div> </div>
@@ -33,18 +29,22 @@
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span> <span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
</div> </div>
<div class="col-span-3 flex items-center justify-center"> <div class="col-span-3 flex items-center justify-center">
<UInputNumber v-model="item.quantity" :min="1" <div class="flex items-center border border-(--border-light) dark:border-(--border-dark) rounded">
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" /> <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>
<div class="col-span-2 text-right"> <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="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 class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</span>
}}</span>
</div> </div>
<div class="col-span-1 flex justify-center"> <div class="col-span-1 flex justify-center">
<button @click="removeItem(item.id)" <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')">
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]" /> <UIcon name="material-symbols:delete" class="text-[20px]" />
</button> </button>
</div> </div>
@@ -53,16 +53,14 @@
<div v-else class="p-8 text-center"> <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" /> <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> <p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
<RouterLink :to="{ name: 'product-card-full' }" <RouterLink :to="{ name: 'product-card-full' }" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Continue Shopping') }} {{ t('Continue Shopping') }}
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
</div> </div>
<div class="lg:w-80"> <div class="lg:w-80">
<div <div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
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> <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="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between"> <div class="flex justify-between">
@@ -76,15 +74,13 @@
</span> </span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) <span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) }}%)</span>
}}%)</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span> <span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div> </div>
</div> </div>
<div class="flex justify-between mb-6"> <div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span> <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">${{ <span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{ cartStore.orderTotal.toFixed(2) }}</span>
cartStore.orderTotal.toFixed(2) }}</span>
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder" <UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
@@ -101,8 +97,7 @@
</div> </div>
<div class="flex flex-col lg:flex-row gap-8"> <div class="flex flex-col lg:flex-row gap-8">
<div class="flex-1"> <div class="flex-1">
<div <div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
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> <h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
<div class="mb-4"> <div class="mb-4">
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')" <UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
@@ -110,7 +105,8 @@
</div> </div>
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3"> <div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
<label v-for="address in addressStore.filteredAddresses" :key="address.id" <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 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-(--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'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress" <input type="radio" :value="address.id" v-model="selectedAddress"
@@ -125,20 +121,19 @@
<div v-else class="text-center py-6"> <div v-else class="text-center py-6">
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" /> <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> <p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }" <RouterLink :to="{ name: 'addresses' }" class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Add Address') }} {{ t('Add Address') }}
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div <div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
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> <h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
<div class="space-y-3"> <div class="space-y-3">
<label v-for="method in cartStore.deliveryMethods" :key="method.id" <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 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-(--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'"> : 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod" <input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
@@ -162,7 +157,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useCartStore } from '@/stores/cart' import { useCartStore, type CartItem } from '@/stores/cart'
import { useAddressStore } from '@/stores/address' import { useAddressStore } from '@/stores/address'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -195,6 +190,15 @@ const canPlaceOrder = computed(() => {
cartStore.selectedAddressId !== null && cartStore.selectedAddressId !== null &&
cartStore.selectedDeliveryMethodId !== 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) { function removeItem(itemId: number) {
cartStore.removeItem(itemId) cartStore.removeItem(itemId)
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="container mt-14 mx-auto"> <div class="container mt-14 mx-auto">
<div class="flex md:flex-row flex-col justify-between gap-8 mb-6"> <div class="flex justify-between gap-8 mb-6">
<div class="flex-1"> <div class="flex-1">
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]"> <div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
<img :src="selectedColor?.image || productData.image" :alt="productData.name" <img :src="selectedColor?.image || productData.image" :alt="productData.name"
@@ -29,7 +29,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4"> <div class="flex justify-between items-end mb-8">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span> <span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -49,10 +49,10 @@
</div> </div>
<ProductCustomization /> <ProductCustomization />
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" /> <hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
<div class="mb-6 w-[100%] xl:w-[60%]"> <div class="mb-6 w-[55%]">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8"> <div class="flex justify-between items-center gap-10 mb-8">
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[ <UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!', 'px-15 py-2 cursor-pointer',
activeTab === tab.id activeTab === tab.id
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white' ? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'

View File

@@ -1,89 +0,0 @@
<template>
<div class="container mx-auto p-6">
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
<div v-if="loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
</div>
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ error }}
</div>
<div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Product ID
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Reference
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Price
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="product in products" :key="product.product_id"
class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.product_id }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.reference }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ product.price.toFixed(2) }}
</td>
</tr>
</tbody>
</table>
<div v-if="products.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
No products found
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
interface Product {
product_id: number
reference: string
price: number
}
interface ApiResponse {
items: Product[]
count: number
message: string
}
const products = ref<Product[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
async function fetchProducts() {
loading.value = true
error.value = null
try {
const response = await useFetchJson<ApiResponse>('/api/v1/restricted/list-products/get-listing?p=3&elems') as unknown as { items: Product[] }
products.value = response.items || []
} catch (e: any) {
error.value = e.message || 'Failed to load products'
console.error('Error fetching products:', e)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchProducts()
})
</script>

View File

@@ -4,7 +4,7 @@
<p class="text-[24px] font-bold">Product customization</p> <p class="text-[24px] font-bold">Product customization</p>
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p> <p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
</div> </div>
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-5 md:gap-10 "> <div class="grid grid-cols-3 gap-10">
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/> <UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/> <UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/> <UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>

View File

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

View File

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

View File

@@ -1,15 +1,20 @@
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
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1') return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
} }
await getSettings() await getSettings()
const routes = await getMenu()
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL), history: createWebHistory(import.meta.env.VITE_BASE_URL),
routes: [ routes: [
@@ -20,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,
@@ -30,50 +36,90 @@ const router = createRouter({
{ 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: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
{ path: 'products-list', component: () => import('../components/customer/PageProductsList.vue'), name: 'products-list' },
], ],
}, },
{ { path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true, } },
path: '',
component: Empty,
children: [
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true } },
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } }, { path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } }, { path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } }, { path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } }, { path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
], // {
// path: '',
// component: Empty,
// children: [
// ],
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
}, },
], ],
}, },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'),
name: 'not-found',
},
], ],
}) })
router.beforeEach((to, from, next) => { // router.beforeEach((to, from, next) => {
// const locale = to.params.locale as string
// const localeLang = langs.find((x) => x.iso_code == locale)
// if (locale && langs.length > 0) {
// const authStore = useAuthStore()
// // console.log(authStore.isAuthenticated, to, from)
// // if()
// const validLocale = langs.find((l) => l.lang_code === locale)
// if (validLocale) {
// currentLang.value = localeLang
// if (!to.meta?.guest && !isAuthenticated()) {
// return next({ name: 'login', params: { locale } })
// }
// // return next()
// return next()
// } else if (locale) {
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
// }
// }
// if (!locale && to.path !== '/') {
// return next(`/${currentLang.value?.iso_code}${to.path}`)
// }
// next()
// })
router.beforeEach((to, from) => {
const locale = to.params.locale as string const locale = to.params.locale as string
const localeLang = langs.find((x) => x.iso_code == locale) const localeLang = langs.find((x) => x.iso_code === locale)
if (locale && langs.length > 0) { if (locale && langs.length > 0) {
const authStore = useAuthStore() const authStore = useAuthStore()
console.log(authStore.isAuthenticated, to, from)
// if()
const validLocale = langs.find((l) => l.lang_code === locale) const validLocale = langs.find((l) => l.lang_code === locale)
if (validLocale) { if (validLocale) {
currentLang.value = localeLang currentLang.value = localeLang
if (!to.meta?.guest && !isAuthenticated()) {
return next({ name: 'login', params: { locale } }) if (!to.meta?.guest && !authStore.isAuthenticated) {
return { name: 'login', params: { locale } }
} }
return next() return true
} else if (locale) { } else if (locale) {
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`) return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
} }
} }
if (!locale && to.path !== '/') { if (!locale && to.path !== '/') {
return next(`/${currentLang.value?.iso_code}${to.path}`) return `/${currentLang.value?.iso_code}${to.path}`
} }
next()
return true
}) })
export default router export default router

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

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

View File

@@ -95,6 +95,7 @@ export const useAddressStore = defineStore('address', () => {
id: existing.id, id: existing.id,
...normalize(formData) ...normalize(formData)
} }
return true return true
} }
function deleteAddress(id: number): boolean { function deleteAddress(id: number): boolean {

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

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

View File

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

View File

@@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useValidation } from '@/composable/useValidation' import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -52,9 +52,11 @@ const PrivacyComponent = computed(() =>
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')), import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
), ),
) )
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false"> <UDrawer v-model:open="showTherms" :overlay="false">
<template #body> <template #body>
<component :is="TermsComponent" /> <component :is="TermsComponent" />
@@ -63,6 +65,7 @@ const PrivacyComponent = computed(() =>
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton> <UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
</template> </template>
</UDrawer> </UDrawer>
<!-- PrivacyPolicyView -->
<UDrawer v-model:open="showPrivacy" :overlay="false"> <UDrawer v-model:open="showPrivacy" :overlay="false">
<template #body> <template #body>
<component :is="PrivacyComponent" /> <component :is="PrivacyComponent" />
@@ -75,9 +78,9 @@ const PrivacyComponent = computed(() =>
<div class="text-center mb-15"> <div class="text-center mb-15">
<div <div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30"> 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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" /> <UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div> </div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div> </div>
<div class="w-full max-w-md flex flex-col gap-4"> <div class="w-full max-w-md flex flex-col gap-4">
<UForm :validate="validate" @submit="handleLogin" class="space-y-5"> <UForm :validate="validate" @submit="handleLogin" class="space-y-5">
@@ -93,12 +96,11 @@ const PrivacyComponent = computed(() =>
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black"> <UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
<UInput v-model="password" :placeholder="$t('general.enter_your_password')" <UInput v-model="password" :placeholder="$t('general.enter_your_password')"
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
:ui="{ trailing: 'pe-1' }">
<template #trailing> <template #trailing>
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'" <UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword" :aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
aria-controls="password" @click="showPassword = !showPassword" class="mr-2" /> aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
</template> </template>
</UInput> </UInput>
</UFormField> </UFormField>
@@ -114,11 +116,15 @@ const PrivacyComponent = computed(() =>
{{ $t('general.sign_in') }} {{ $t('general.sign_in') }}
</UButton> </UButton>
</UForm> </UForm>
<!-- Divider -->
<div class="flex items-center gap-3 my-1"> <div class="flex items-center gap-3 my-1">
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" /> <div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span> <span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" /> <div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
</div> </div>
<!-- Google Sign In -->
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading" <UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
@click="authStore.loginWithGoogle()" @click="authStore.loginWithGoogle()"
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer"> class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
@@ -159,4 +165,5 @@ const PrivacyComponent = computed(() =>
</p> </p>
</div> </div>
</div> </div>
</component>
</template> </template>

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
<template> <template>
<component :is="Empty || 'div'">
<UDrawer v-model:open="showTherms" :overlay="false"> <UDrawer v-model:open="showTherms" :overlay="false">
<template #body> <template #body>
<component :is="TermsComponent" /> <component :is="TermsComponent" />
@@ -21,9 +22,9 @@
<div class="text-center mb-15"> <div class="text-center mb-15">
<div <div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30"> 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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" /> <UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div> </div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div> </div>
<div class="w-full max-w-md"> <div class="w-full max-w-md">
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3"> <UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
@@ -110,6 +111,7 @@
</UForm> </UForm>
</div> </div>
</div> </div>
</component>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -120,6 +122,8 @@ import { useValidation } from '@/composable/useValidation'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { locale } = useI18n() const { locale } = useI18n()
const router = useRouter() const router = useRouter()

View File

@@ -181,7 +181,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
</script> </script>
<template> <template>
<div class="container mx-auto"> <div class="container">
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans"> <div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }} <h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
</h1> </h1>

View File

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

View File

@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import Empty from '@/layouts/empty.vue'
const { t, te } = useI18n() const { t, te } = useI18n()
const router = useRouter() const router = useRouter()
@@ -66,6 +67,7 @@ function goToLogin() {
</script> </script>
<template> <template>
<component :is="Empty || 'div'">
<div <div
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900"> class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8"> <div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
@@ -73,9 +75,9 @@ function goToLogin() {
<div class="text-center mb-8"> <div class="text-center mb-8">
<div <div
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30"> 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="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" /> <UIcon name="i-heroicons-clock" class="w-8 h-8" />
</div> </div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
</div> </div>
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50"> <UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
@@ -133,7 +135,8 @@ function goToLogin() {
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
{{ $t('verify_email.already_registered') }} {{ $t('verify_email.already_registered') }}
<button variant="link" size="sm" @click="goToLogin" <button variant="link" size="sm" @click="goToLogin"
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in') class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
$t('general.sign_in')
}} }}
</button> </button>
</p> </p>
@@ -143,4 +146,5 @@ function goToLogin() {
</div> </div>
</div> </div>
</div> </div>
</component>
</template> </template>

1
go.mod
View File

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

2
go.sum
View File

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

View File

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

View File

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