Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into product-procedures

This commit is contained in:
2026-03-27 08:47:41 +01:00
25 changed files with 614 additions and 729 deletions

View File

@@ -24,6 +24,7 @@ type Config struct {
GoogleTranslate GoogleTranslateConfig GoogleTranslate GoogleTranslateConfig
Image ImageConfig Image ImageConfig
Cors CorsConfig Cors CorsConfig
MailiSearch MeiliSearchConfig
} }
type I18n struct { type I18n struct {
@@ -38,6 +39,11 @@ type CorsConfig struct {
Origins []string `env:"CORS_ORGIN"` Origins []string `env:"CORS_ORGIN"`
} }
type MeiliSearchConfig struct {
ServerURL string `env:"MEILISEARCH_URL"`
ApiKey string `env:"MEILISEARCH_API_KEY"`
}
type ImageConfig struct { type ImageConfig struct {
ImagePrefix string `env:"IMAGE_PREFIX"` ImagePrefix string `env:"IMAGE_PREFIX"`
} }
@@ -186,6 +192,12 @@ func load() *Config {
if err != nil { if err != nil {
slog.Error("not possible to load env variables for google translate : ", err.Error(), "") slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
} }
err = loadEnv(&cfg.MailiSearch)
if err != nil {
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
}
return cfg return cfg
} }

View File

@@ -28,6 +28,7 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
// for testing purposes only. Must be removed before proper release. // for testing purposes only. Must be removed before proper release.
r.Get("/create-index", handler.CreateIndex) r.Get("/create-index", handler.CreateIndex)
r.Get("/test", handler.Test) r.Get("/test", handler.Test)
r.Get("/settings", handler.GetSettings)
// for all users // for all users
r.Get("/search", handler.Search) r.Get("/search", handler.Search)
@@ -77,13 +78,6 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
query := c.Query("query") query := c.Query("query")
limit_attribute := c.Query("limit")
limit, err := strconv.Atoi(limit_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
id_category_attribute := c.Query("id_category", "0") id_category_attribute := c.Query("id_category", "0")
id_category, err := strconv.Atoi(id_category_attribute) id_category, err := strconv.Atoi(id_category_attribute)
if err != nil { if err != nil {
@@ -91,21 +85,7 @@ func (h *MeiliSearchHandler) Search(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)))
} }
price_lower_bound_attribute := c.Query("price_lower_bound", "-1.0") meili_response, err := h.meiliService.Search(id_lang, query, uint(id_category))
price_lower_bound, err := strconv.ParseFloat(price_lower_bound_attribute, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
price_upper_bound_attribute := c.Query("price_upper_bound", "-1.0")
price_upper_bound, err := strconv.ParseFloat(price_upper_bound_attribute, 64)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
meili_response, err := h.meiliService.Search(id_lang, query, uint(limit), uint(id_category), price_lower_bound, price_upper_bound)
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)))
@@ -113,3 +93,19 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
return c.JSON(response.Make(&meili_response, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&meili_response, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error {
id_lang, ok := c.Locals("langID").(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
settings, err := h.meiliService.GetIndexSettings(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(&settings, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -30,8 +30,6 @@ type ProductRow struct {
type MeiliSearchProduct struct { type MeiliSearchProduct struct {
ProductID uint `gorm:"column:id_product"` ProductID uint `gorm:"column:id_product"`
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
Active uint8 `gorm:"column:active"`
Price float64 `gorm:"column:price"`
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
DescriptionShort string `gorm:"column:description_short"` DescriptionShort string `gorm:"column:description_short"`
Usage string `gorm:"column:used_for"` Usage string `gorm:"column:used_for"`
@@ -44,4 +42,6 @@ type MeiliSearchProduct struct {
CategoryID uint `gorm:"column:id_category"` CategoryID uint `gorm:"column:id_category"`
CategoryName string `gorm:"column:category_name"` CategoryName string `gorm:"column:category_name"`
Variations uint `gorm:"column:variations"` Variations uint `gorm:"column:variations"`
CategoryIDs []uint `gorm:"-"` // All category IDs including children for filtering
CoverImage string `gorm:"-"` // Cover image URL (not indexed)
} }

View File

@@ -1,15 +1,17 @@
package model package model
import "encoding/json"
type B2BTopMenu struct { type B2BTopMenu struct {
MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"` MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"`
Label string `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"` Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"`
ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"` ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"`
Params string `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"` Params string `gorm:"column:params;type:longtext;not null;default:'{}'" json:"params"`
Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"` Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"`
Position int `gorm:"column:position;not null;default:1" json:"position"` Position int `gorm:"column:position;not null;default:1" json:"position"`
Parent *B2BTopMenu `gorm:"foreignKey:ParentID;references:MenuID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"parent,omitempty"` Parent *B2BTopMenu `gorm:"foreignKey:ParentID;references:MenuID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"parent,omitempty"`
Children []B2BTopMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"` Children []*B2BTopMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"`
} }
func (B2BTopMenu) TableName() string { func (B2BTopMenu) TableName() string {

View File

@@ -79,36 +79,96 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) { func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct var products []model.MeiliSearchProduct
err := db.DB. err := db.DB.Debug().
// Select(`
// ps.id_product AS id_product,
// pl.name AS name,
// ps.price AS price,
// pl.description AS description,
// pl.description_short AS description_short,
// pl.usage AS used_for,
// p.ean13 AS ean13,
// p.reference AS reference,
// p.width AS width,
// p.height AS height,
// p.depth AS depth,
// p.weight AS weight,
// ps.id_category_default AS id_category,
// cl.name AS category_name,
// COUNT(DISTINCT pas.id_product_attribute) AS variations
// `).
// Table("ps_product_shop AS ps").
// Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
// Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
// Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
// Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID).
// Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID).
// Group("ps.id_product").
Select(` Select(`
ps.id_product AS id_product, ps.id_product AS id_product,
pl.name AS name, pl.name AS name,
ps.active AS active,
ps.price AS price,
pl.description AS description, pl.description AS description,
pl.description_short AS description_short, pl.description_short AS description_short,
pl.usage AS used_for, pl.usage AS used_for,
p.ean13 AS ean13, p.ean13 AS ean13,
p.reference AS reference, p.reference AS reference,
p.width AS width,
p.height AS height,
p.depth AS depth,
p.weight AS weight,
ps.id_category_default AS id_category, ps.id_category_default AS id_category,
cl.name AS category_name, cl.name AS category_name,
COUNT(DISTINCT pas.id_product_attribute) AS variations cl.link_rewrite as link_rewrite,
COUNT(DISTINCT pas.id_product_attribute) AS variations,
pis.id_image AS id_image,
GROUP_CONCAT(DISTINCT pcp.id_category) AS category_ids
`). `).
Table("ps_product_shop AS ps"). Table("ps_product_shop AS ps").
Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID).
Where("ps.id_shop = ?", constdata.SHOP_ID). Joins("JOIN ps_image_shop AS pis ON pis.id_product = ps.id_product AND pis.cover = 1").
Group("ps.id_product"). Joins("JOIN ps_category_product AS pcp ON pcp.id_product = ps.id_product").
Where("ps.id_shop = ? AND ps.active = 1", constdata.SHOP_ID).
Group("pcp.id_product, ps.id_product").
Scan(&products).Error Scan(&products).Error
if err != nil { if err != nil {
return products, fmt.Errorf("database error: %w", err) return products, fmt.Errorf("database error: %w", err)
} }
// Get all category IDs for each product (including child categories)
for i := range products {
var categoryIDs []uint
// Find all parent categories and their children using nested set
err := db.DB.
Table("ps_category AS c").
Select("c.id_category").
Joins("JOIN ps_category_product AS cp ON cp.id_category = c.id_category").
Joins("JOIN ps_category AS parent ON c.nleft >= parent.nleft AND c.nright <= parent.nright AND parent.id_category = ?", products[i].CategoryID).
Where("cp.id_product = ?", products[i].ProductID).
Group("c.id_category").
Pluck("c.id_category", &categoryIDs).Error
if err != nil {
continue // Skip if error, use default category
}
if len(categoryIDs) > 0 {
products[i].CategoryIDs = categoryIDs
} else {
products[i].CategoryIDs = []uint{products[i].CategoryID}
}
// Get cover image for the product
var imageID int
err = db.DB.
Table("ps_image AS i").
Select("i.id_image").
Joins("LEFT JOIN ps_image_shop AS iss ON iss.id_image = i.id_image AND iss.id_shop = ?", constdata.SHOP_ID).
Where("i.id_product = ? AND (i.cover = 1 OR i.cover IS TRUE)", products[i].ProductID).
Order("i.position ASC").
Limit(1).
Pluck("i.id_image", &imageID).Error
if err == nil && imageID > 0 {
products[i].CoverImage = fmt.Sprintf("%d/%d.jpg", products[i].ProductID, imageID)
}
}
return products, nil return products, nil
} }

View File

@@ -1,14 +1,13 @@
package meiliService package meiliService
import ( import (
"encoding/xml"
"fmt" "fmt"
"io" "regexp"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
@@ -20,12 +19,10 @@ type MeiliService struct {
} }
func New() *MeiliService { func New() *MeiliService {
meiliURL := os.Getenv("MEILISEARCH_URL")
meiliAPIKey := os.Getenv("MEILISEARCH_API_KEY")
client := meilisearch.New( client := meilisearch.New(
meiliURL, config.Get().MailiSearch.ServerURL,
meilisearch.WithAPIKey(meiliAPIKey), meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey),
) )
return &MeiliService{ return &MeiliService{
@@ -40,32 +37,9 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang) products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
for i := 0; i < len(products); i++ { for i := 0; i < len(products); i++ {
products[i].Description, err = cleanHTML(products[i].Description) products[i].Description = cleanHTML(products[i].Description)
if err != nil { products[i].DescriptionShort = cleanHTML(products[i].DescriptionShort)
fmt.Printf("products[i].Description: %v\n", products[i].Description) products[i].Usage = cleanHTML(products[i].Usage)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description")
fmt.Printf("err: %v\n", err)
return err
}
products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
if err != nil {
fmt.Printf("products[i].DescriptionShort: %v\n", products[i].DescriptionShort)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at description short")
fmt.Printf("err: %v\n", err)
return err
}
products[i].Usage, err = cleanHTML(products[i].Usage)
if err != nil {
fmt.Printf("products[i].Usage: %v\n", products[i].Usage)
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
fmt.Println("failed at usage")
fmt.Printf("err: %v\n", err)
return err
}
} }
primaryKey := "ProductID" primaryKey := "ProductID"
@@ -83,7 +57,7 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
filterableAttributes := []interface{}{ filterableAttributes := []interface{}{
"CategoryID", "CategoryID",
"Price", "CategoryIDs",
} }
task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes) task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
if err != nil { if err != nil {
@@ -96,11 +70,10 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
displayedAttributes := []string{ displayedAttributes := []string{
"ProductID", "ProductID",
"Name", "Name",
"Active",
"Price",
"EAN13", "EAN13",
"Reference", "Reference",
"Variations", "Variations",
"CoverImage",
} }
task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes) task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes)
if err != nil { if err != nil {
@@ -154,28 +127,17 @@ func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
} }
// Search performs a full-text search on the specified index // Search performs a full-text search on the specified index
func (s *MeiliService) Search(id_lang uint, query string, limit uint, id_category uint, price_lower_bound float64, price_upper_bound float64) (meilisearch.SearchResponse, error) { func (s *MeiliService) Search(id_lang uint, query string, id_category uint) (meilisearch.SearchResponse, error) {
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
filter_query := "" filter_query := "Active = 1"
if id_category != 0 { if id_category != 0 {
filter_query = "CategoryID = " + strconv.FormatUint(uint64(id_category), 10) // Use CategoryIDs to include products from child categories
} filter_query += fmt.Sprintf(" AND CategoryIDs = %d", id_category)
if price_lower_bound > 0.0 {
if filter_query != "" {
filter_query += " AND "
}
filter_query += "Price >= " + strconv.FormatFloat(price_lower_bound, 'f', -1, 64)
}
if price_upper_bound > 0.0 {
if filter_query != "" {
filter_query += " AND "
}
filter_query += "Price <= " + strconv.FormatFloat(price_upper_bound, 'f', -1, 64)
} }
searchReq := &meilisearch.SearchRequest{ searchReq := &meilisearch.SearchRequest{
Limit: int64(limit), Limit: 30,
Facets: []string{ Facets: []string{
"CategoryID", "CategoryID",
}, },
@@ -201,40 +163,59 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
return health, nil return health, nil
} }
// remove all tags from HTML text // GetIndexSettings retrieves the current settings for a specific index
func cleanHTML(s string) (string, error) { func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) {
r := strings.NewReader(s) indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
d := xml.NewDecoder(r)
text := "" index := s.meiliClient.Index(indexName)
// Configure the decoder for HTML; leave off strict and autoclose for XHTML result := make(map[string]interface{})
d.Strict = true
d.AutoClose = xml.HTMLAutoClose // Get searchable attributes
d.Entity = xml.HTMLEntity searchable, err := index.GetSearchableAttributes()
for { if err == nil {
token, err := d.Token() result["searchableAttributes"] = searchable
if err == io.EOF {
break
} else if err != nil {
return text, err
} }
switch v := token.(type) { // Get filterable attributes
case xml.StartElement: filterable, err := index.GetFilterableAttributes()
if len(text) > 0 && text[len(text)-1] != '\n' { if err == nil {
text += " \n " result["filterableAttributes"] = filterable
}
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 // Get displayed attributes
displayed, err := index.GetDisplayedAttributes()
if err == nil {
result["displayedAttributes"] = displayed
}
// Get ranking rules
ranking, err := index.GetRankingRules()
if err == nil {
result["rankingRules"] = ranking
}
// Get distinct attribute
distinct, err := index.GetDistinctAttribute()
if err == nil && distinct != nil {
result["distinctAttribute"] = *distinct
}
// Get typo tolerance
typo, err := index.GetTypoTolerance()
if err == nil {
result["typoTolerance"] = typo
}
return result, nil
}
// remove all tags from HTML text
func cleanHTML(s string) string {
// Simple regex to remove all HTML tags
re := regexp.MustCompile(`<[^>]*>`)
result := re.ReplaceAllString(s, "")
// Replace multiple spaces with single space
result = regexp.MustCompile(`\s+`).ReplaceAllString(result, " ")
return strings.TrimSpace(result)
} }

View File

@@ -98,40 +98,37 @@ 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) 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 } func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
func (s *MenuService) GetTopMenu(id uint) ([]model.B2BTopMenu, error) { func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) {
items, err := s.routesRepo.GetTopMenu(id) items, err := s.routesRepo.GetTopMenu(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(items) == 0 { menuMap := make(map[int]*model.B2BTopMenu, len(items))
return []model.B2BTopMenu{}, nil roots := make([]*model.B2BTopMenu, 0)
}
// Build a map with empty children slices
itemMap := make(map[int]model.B2BTopMenu, len(items))
for i := range items { for i := range items {
items[i].Children = []model.B2BTopMenu{} menu := &items[i]
itemMap[items[i].MenuID] = items[i] menu.Children = make([]*model.B2BTopMenu, 0)
menuMap[menu.MenuID] = menu
} }
// Build the tree for i := range items {
var roots []model.B2BTopMenu menu := &items[i]
for _, item := range itemMap {
if item.ParentID == nil || *item.ParentID == 0 { if menu.ParentID == nil {
roots = append(roots, itemMap[item.MenuID]) roots = append(roots, menu)
} else { continue
parentID := *item.ParentID
if parent, exists := itemMap[parentID]; exists {
parent.Children = append(parent.Children, item)
itemMap[parentID] = parent
}
}
} }
// Update roots with children parent, ok := menuMap[*menu.ParentID]
for i := range roots { if !ok {
roots[i] = itemMap[roots[i].MenuID] // fallback for orphaned nodes
roots = append(roots, menu)
continue
}
parent.Children = append(parent.Children, menu)
} }
return roots, nil return roots, nil

9
bo/components.d.ts vendored
View File

@@ -12,18 +12,27 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Cart1: typeof import('./src/components/customer/Cart1.vue')['default'] Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
copy: typeof import('./src/components/inner/categoryMenu copy.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default'] En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default'] LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
Page: typeof import('./src/components/customer/Page.vue')['default']
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']
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default'] PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default']
PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default'] PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default']
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default'] PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
PageProductsList: typeof import('./src/components/admin/PageProductsList.vue')['default'] PageProductsList: typeof import('./src/components/admin/PageProductsList.vue')['default']
PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default']
PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.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']

View File

@@ -1,14 +1,50 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFetchJson } from '@/composable/useFetchJson'
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/langSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/themeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import type { ApiResponse } from '@/types'
import { computed, ref } from 'vue'
import { currentLang } from '@/router/langs'
const authStore = useAuthStore() const authStore = useAuthStore()
let menu = ref()
async function getTopMenu() {
try {
const { items } = await useFetchJson<ApiResponse>('/api/v1/restricted/menu/get-top-menu')
menu.value = items
} catch (err) {
console.log(err);
}
}
const menuItems = computed(() =>
transformMenu(menu.value[0].children, currentLang.value?.iso_code)
)
function transformMenu(items, locale: string | undefined) {
return items.map((item) => {
const parsedLabel = JSON.parse(item.label)
return {
icon: 'i-lucide-house',
label: parsedLabel.trans[locale] || parsedLabel.name,
to: { name: parsedLabel.name },
children: item.children
? transformMenu(item.children, locale)
: undefined,
}
})
}
await getTopMenu()
</script> </script>
<template> <template>
{{ menuItems }}
<!-- fixed top-0 left-0 right-0 z-50 -->
<header <header
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)"> class=" bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14"> <div class="flex items-center justify-between h-14">
<!-- Logo --> <!-- Logo -->
@@ -18,28 +54,39 @@ const authStore = useAuthStore()
</div> </div>
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink> </RouterLink>
<!-- Right Side Actions -->
<RouterLink :to="{ name: 'products-list' }"> <UNavigationMenu :items="menuItems" />
<!-- {{ router }} -->
<!-- <RouterLink :to="{ name: 'admin-products' }">
products list products list
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'product-detail' }"> <RouterLink :to="{ name: 'admin-product-detail' }">
product detail product detail
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'product-card-full' }"> <RouterLink :to="{
ProductCardFull name: 'customer-product', params: {
product_id: '51'
}
}">
Product (51)
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'addresses' }"> <RouterLink :to="{ name: 'addresses' }">
Addresses Addresses
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'customer-data' }"> <RouterLink :to="{ name: 'profile-details' }">
Customer Data Customer Data
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'cart' }"> <RouterLink :to="{ name: 'cart' }">
Cart Cart
</RouterLink> </RouterLink>
<RouterLink :to="{ name: 'cart1' }"> <RouterLink :to="{
Cart1 name: 'cart-details', params: {
</RouterLink> cart_id: '1'
}
}">
Cart details (1)
</RouterLink> -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher --> <!-- Language Switcher -->
<LangSwitch /> <LangSwitch />

View File

@@ -10,7 +10,6 @@
</div> </div>
</template> </template>
</UNavigationMenu> --> </UNavigationMenu> -->
{{ filters }}
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1> <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"> <div v-if="loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span> <span class="text-gray-600 dark:text-gray-400">Loading products...</span>
@@ -19,13 +18,16 @@
{{ error }} {{ error }}
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<div class="flex gap-2">
<CategoryMenuListing />
<UTable :data="productsList" :columns="columns" class="flex-1"> <UTable :data="productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }"> <template #expanded="{ row }">
<UTable :data="productsList" :columns="columnsChild" :ui="{ <UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
thead: 'hidden' thead: 'hidden'
}" /> }" />
</template> </template>
</UTable> </UTable>
</div>
<div class="flex justify-center items-center py-8"> <div class="flex justify-center items-center py-8">
<UPagination v-model:page="page" :total="total" :page-size="perPage" /> <UPagination v-model:page="page" :total="total" :page-size="perPage" />
</div> </div>
@@ -39,12 +41,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch, h, resolveComponent, computed } from 'vue' import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import { log } from 'console' import CategoryMenuListing from '../inner/categoryMenuListing.vue'
interface Product { interface Product {
reference: number reference: number
@@ -98,6 +100,7 @@ const sortField = computed({
router.push({ query }) router.push({ query })
} }
}) })
const perPage = ref(15) const perPage = ref(15)
const total = ref(0) const total = ref(0)
@@ -110,32 +113,64 @@ interface ApiResponse {
const productsList = ref<Product[]>([]) const productsList = ref<Product[]>([])
const loading = ref(true) const loading = ref(true)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const filters = ref<Record<string, string>>({}) const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(key => {
if (!['page', 'sort', 'direction'].includes(key)) {
delete baseQuery[key]
}
})
router.push({
query: {
...baseQuery,
...val,
page: 1
}
})
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
async function fetchProductList() { async function fetchProductList() {
loading.value = true loading.value = true
error.value = null error.value = null
const [sort, direction] = sortField.value const params = new URLSearchParams()
const params = new URLSearchParams({ Object.entries(route.query).forEach(([key, value]) => {
p: String(page.value), if (value) params.append(key, String(value))
elems: String(perPage.value)
}) })
if (sort && direction) { const url = `/api/v1/restricted/list-products/get-listing?${params}`
params.append('sort', `${sort},${direction}`)
}
Object.entries(filters.value).forEach(([key, value]) => {
if (value) params.append(key, value)
})
const url = `/api/v1/restricted/list-products/get-listing?${params.toString()}`
try { try {
const response = await useFetchJson<ApiResponse>(url) const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || [] productsList.value = response.items || []
total.value = response.count || 0 total.value = response.count || 0
} catch (e: unknown) { } catch (e: unknown) {
@@ -207,13 +242,9 @@ const columns: TableColumn<Payment>[] = [
h(UInput, { h(UInput, {
placeholder: 'Search...', placeholder: 'Search...',
modelValue: column.getFilterValue() ?? '', modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => { 'onUpdate:modelValue': (val: string) => {
if (val) { updateFilter(column.id, val)
filters.value[column.id] = val
} else {
delete filters.value[column.id]
}
}, },
size: 'xs' size: 'xs'
}) })
@@ -250,13 +281,9 @@ const columns: TableColumn<Payment>[] = [
h(UInput, { h(UInput, {
placeholder: 'Search...', placeholder: 'Search...',
modelValue: column.getFilterValue() ?? '', modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => { 'onUpdate:modelValue': (val: string) => {
if (val) { updateFilter(column.id, val)
filters.value[column.id] = val
} else {
delete filters.value[column.id]
}
}, },
size: 'xs' size: 'xs'
}) })
@@ -393,7 +420,11 @@ const columnsChild: TableColumn<Payment>[] = [
} }
] ]
watch([page, sortField, filters.value], () => { watch(
() => route.query,
() => {
fetchProductList() fetchProductList()
}, { immediate: true }) },
{ immediate: true }
)
</script> </script>

View File

@@ -1,5 +1,4 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container my-10 mx-auto "> <div class="container my-10 mx-auto ">

View File

@@ -1,243 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue'
import Default from '@/layouts/default.vue'
const router = useRouter()
const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n()
const searchName = ref('')
const searchCode = ref('')
const priceFromFilter = ref<number | null>(null)
const priceToFilter = ref<number | null>(null)
// Pagination
const page = ref(1)
const pageSize = 5
// Fetch products on mount
// onMounted(() => {
// productStore.getProductDescription(langID: , productID.value)
// })
// Filtered products
// const filteredProducts = computed(() => {
// console.log(productStore.products);
// return productStore.products.filter(product => {
// const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
// const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
// const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
// const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
// return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
// })
// })
// const totalItems = computed(() => filteredProducts.value.length)
// const paginatedProducts = computed(() => {
// const start = (page.value - 1) * pageSize
// const end = start + pageSize
// return filteredProducts.value.slice(start, end)
// })
// Reset page when filters change
function resetPage() {
page.value = 1
}
// Navigate to product detail
function goToProduct(product: Product) {
router.push({ name: 'product-detail', params: { id: product.id } })
}
// Table columns
const columns = computed<TableColumn<Product>[]>(() => [
{
accessorKey: 'image',
header: () => h('div', { class: 'text-center' }, t('products.image')),
cell: ({ row }) => h('img', {
src: row.getValue('image'),
alt: 'Product',
class: 'w-12 h-12 object-cover rounded'
})
},
{
accessorKey: 'name',
header: t('products.product_name'),
cell: ({ row }) => {
const product = row.original
return h('button', {
class: 'text-primary hover:underline font-medium text-left',
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
}, product.name)
}
},
{
accessorKey: 'code',
header: t('products.product_code'),
},
{
accessorKey: 'description',
header: t('products.description'),
cell: ({ row }) => {
const desc = row.getValue('description') as string
return h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, desc?.substring(0, 50) + (desc && desc.length > 50 ? '...' : ''))
}
},
{
accessorKey: 'inStock',
header: t('products.in_stock'),
cell: ({ row }) => {
const inStock = row.getValue('inStock')
return h('span', {
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
}, inStock ? t('products.yes') : t('products.no'))
}
},
{
accessorKey: 'price',
header: t('products.price'),
cell: ({ row }) => {
const priceFromVal = row.original.priceFrom
const priceToVal = row.original.priceTo
return `${priceFromVal} - ${priceToVal}`
}
},
{
accessorKey: 'count',
header: t('products.count'),
},
{
id: 'actions',
header: '',
cell: ({ row }) => {
const product = row.original
return h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
}, t('products.add_to_cart')),
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
}, '+')
])
}
}
])
// Actions
function addToCart(product: Product) {
console.log('Add to cart:', product)
}
function incrementCount(product: Product) {
product.count++
}
function clearFilters() {
searchName.value = ''
searchCode.value = ''
priceFromFilter.value = null
priceToFilter.value = null
resetPage()
}
</script>
<template>
<component :is="Default || 'div'">
<div class="container">
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<div>
<!-- v-html="productStore.products" -->
</div>
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
{{ t('products.login_to_view') }}
</div>
<!-- Loading State -->
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<!-- Error State -->
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
<!-- Filter Block -->
<div
class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name')
}}</label>
<UInput v-model="searchName" :placeholder="t('products.search_name_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code')
}}</label>
<UInput v-model="searchCode" :placeholder="t('products.search_code_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
<UInput v-model="priceFromFilter" type="number" :placeholder="t('products.price_from')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
<UInput v-model="priceToFilter" type="number" :placeholder="t('products.price_to')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex items-end">
<button @click="clearFilters"
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
{{ t('products.clear_filters') }}
</button>
</div>
</div>
<!-- Products Table -->
<!-- <div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
<UTable
:data="paginatedProducts"
:columns="columns"
class="dark:text-white! text-dark"
/>
</div> -->
<!-- Empty State -->
<!-- <div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
{{ t('products.no_products') }}
</div> -->
<!-- Pagination -->
<!-- <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination
v-model:page="page"
:page-count="pageSize"
:total="totalItems"
/>
</div> -->
<!-- Results count -->
<!-- <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
{{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
</div> -->
</div>
</div>
</div>
</component>
</template>

View File

@@ -65,7 +65,7 @@ function handleCancel() {
} }
function handleContinueToCheckout() { function handleContinueToCheckout() {
router.push({ name: 'cart' }) router.push({ name: 'carts' })
} }
function removeItem(itemId: number) { function removeItem(itemId: number) {

View File

@@ -1,201 +0,0 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<h2
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
{{ t('Selected Products') }}
</h2>
<div v-if="cartStore.items.length > 0">
<div v-for="item in cartStore.items" :key="item.id"
class="grid grid-cols-5 items-center p-4 border-b border-(--border-light) dark:border-(--border-dark) w-[100%]">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
</div>
<p class="text-black dark:text-white text-sm font-medium">{{ item.name }}</p>
<p class="text-black dark:text-white">${{ item.price.toFixed(2) }}</p>
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
}}</p>
<div class="flex items-center justify-end gap-10">
<UInputNumber v-model="item.quantity" :min="1"
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
<div class="flex justify-center">
<button @click="removeItem(item.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[20px]" />
</button>
</div>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
<RouterLink :to="{ name: 'product-card-full' }"
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Continue Shopping') }}
</RouterLink>
</div>
</div>
</div>
<div class="lg:w-80">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Shipping') }}</span>
<span class="text-black dark:text-white">
{{ cartStore.shippingCost > 0 ? `$${cartStore.shippingCost.toFixed(2)}` : t('Free') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
}}%)</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div>
</div>
<div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
cartStore.orderTotal.toFixed(2) }}</span>
</div>
<div class="flex flex-col gap-3">
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light) disabled:opacity-50 disabled:cursor-not-allowed">
{{ t('Place Order') }}
</UButton>
<UButton block variant="outline" color="neutral" @click="cancelOrder"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
<div class="mb-4">
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
</div>
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress"
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}</p>
</div>
</label>
</div>
<div v-else class="text-center py-6">
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }"
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Add Address') }}
</RouterLink>
</div>
</div>
</div>
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
<div class="space-y-3">
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-black dark:text-white font-medium">{{ method.name }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium">
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
</span>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ method.description }}</p>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useAddressStore } from '@/stores/address'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const addressStore = useAddressStore()
const { t } = useI18n()
const router = useRouter()
const selectedAddress = ref<number | null>(cartStore.selectedAddressId)
const selectedDeliveryMethod = ref<number | null>(cartStore.selectedDeliveryMethodId)
const addressSearchQuery = ref('')
watch(addressSearchQuery, (val) => {
addressStore.setSearchQuery(val)
})
watch(selectedAddress, (newValue) => {
cartStore.setSelectedAddress(newValue)
})
watch(selectedDeliveryMethod, (newValue) => {
if (newValue) {
cartStore.setDeliveryMethod(newValue)
}
})
const canPlaceOrder = computed(() => {
return cartStore.items.length > 0 &&
cartStore.selectedAddressId !== null &&
cartStore.selectedDeliveryMethodId !== null
})
function removeItem(itemId: number) {
cartStore.removeItem(itemId)
}
function placeOrder() {
if (canPlaceOrder.value) {
console.log('Placing order...')
alert(t('Order placed successfully!'))
cartStore.clearCart()
router.push({ name: 'home' })
}
}
function cancelOrder() {
router.back()
}
</script>

View File

@@ -0,0 +1,204 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<h2
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
{{ t('Selected Products') }}
</h2>
<div v-if="cartStore.items.length > 0">
<div v-for="item in cartStore.items" :key="item.id"
class="grid grid-cols-5 items-center p-4 border-b border-(--border-light) dark:border-(--border-dark) w-[100%]">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
</div>
<p class="text-black dark:text-white text-sm font-medium">{{ item.name }}</p>
<p class="text-black dark:text-white">${{ item.price.toFixed(2) }}</p>
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
}}</p>
<div class="flex items-center justify-end gap-10">
<UInputNumber v-model="item.quantity" :min="1"
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
<div class="flex justify-center">
<button @click="removeItem(item.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[20px]" />
</button>
</div>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
<RouterLink :to="{
name: 'customer-product', params: {
product_id: '51'
}
}" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Continue Shopping') }}
</RouterLink>
</div>
</div>
</div>
<div class="lg:w-80">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Shipping') }}</span>
<span class="text-black dark:text-white">
{{ cartStore.shippingCost > 0 ? `$${cartStore.shippingCost.toFixed(2)}` : t('Free') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
}}%)</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div>
</div>
<div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
cartStore.orderTotal.toFixed(2) }}</span>
</div>
<div class="flex flex-col gap-3">
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light) disabled:opacity-50 disabled:cursor-not-allowed">
{{ t('Place Order') }}
</UButton>
<UButton block variant="outline" color="neutral" @click="cancelOrder"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-10">
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
<div class="mb-4">
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
</div>
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="address.id" v-model="selectedAddress"
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }}, {{ address.city }}</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}</p>
</div>
</label>
</div>
<div v-else class="text-center py-6">
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
<RouterLink :to="{ name: 'addresses' }"
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Add Address') }}
</RouterLink>
</div>
</div>
</div>
<div class="flex-1">
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
<div class="space-y-3">
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
<div class="flex-1">
<div class="flex justify-between items-center">
<span class="text-black dark:text-white font-medium">{{ method.name }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium">
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
</span>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ method.description }}</p>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useAddressStore } from '@/stores/address'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const addressStore = useAddressStore()
const { t } = useI18n()
const router = useRouter()
const selectedAddress = ref<number | null>(cartStore.selectedAddressId)
const selectedDeliveryMethod = ref<number | null>(cartStore.selectedDeliveryMethodId)
const addressSearchQuery = ref('')
watch(addressSearchQuery, (val) => {
addressStore.setSearchQuery(val)
})
watch(selectedAddress, (newValue) => {
cartStore.setSelectedAddress(newValue)
})
watch(selectedDeliveryMethod, (newValue) => {
if (newValue) {
cartStore.setDeliveryMethod(newValue)
}
})
const canPlaceOrder = computed(() => {
return cartStore.items.length > 0 &&
cartStore.selectedAddressId !== null &&
cartStore.selectedDeliveryMethodId !== null
})
function removeItem(itemId: number) {
cartStore.removeItem(itemId)
}
function placeOrder() {
if (canPlaceOrder.value) {
console.log('Placing order...')
alert(t('Order placed successfully!'))
cartStore.clearCart()
router.push({ name: 'home' })
}
}
function cancelOrder() {
router.back()
}
</script>

View File

@@ -118,6 +118,6 @@ const companyAddress = computed(() => {
}) })
function goToCreateAccount() { function goToCreateAccount() {
router.push({ name: 'create-account' }) router.push({ name: 'profile-details-add-info' })
} }
</script> </script>

View File

@@ -173,9 +173,9 @@ function saveAccount() {
companyAddress : '' companyAddress : ''
}) })
router.push({ name: 'customer-data' }) router.push({ name: 'profile-details' })
} }
function goBack() { function goBack() {
router.push({ name: 'customer-data' }) router.push({ name: 'profile-details' })
} }
</script> </script>

View File

@@ -8,11 +8,11 @@ const openAll = ref(false)
function adaptMenu(menu: NavigationMenuItem[]) { function adaptMenu(menu: NavigationMenuItem[]) {
for (const item of menu) { for (const item of menu) {
if(item.children && item.children.length > 0){ if (item.children && item.children.length > 0) {
console.log(item); console.log(item);
adaptMenu(item.children); adaptMenu(item.children);
item.open = openAll.value item.open = openAll.value
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }}) item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
} else { } else {
item.to = { name: 'category', params: item.params }; item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text' item.icon = 'i-lucide-file-text'
@@ -29,12 +29,8 @@ const items = ref<NavigationMenuItem[][]>([
], ],
]) ])
</script> </script>
<template> <template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4"> <UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
</UNavigationMenu>
</template> </template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
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) {
adaptMenu(item.children);
item.open = openAll.value
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
} else {
item.to = { name: 'category', params: item.params };
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
menu = adaptMenu(menu)
const items = ref<NavigationMenuItem[][]>([
[
...menu as NavigationMenuItem[]
],
])
</script>
<template>
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
</template>

View File

@@ -1,4 +1,5 @@
<template> <template>
{{ locale }}
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" <USelectMenu v-model="locale" :items="langs" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!"
valueKey="iso_code" :searchInput="false"> valueKey="iso_code" :searchInput="false">
<template #default="{ modelValue }"> <template #default="{ modelValue }">

View File

@@ -2,17 +2,13 @@ import { createRouter, createWebHistory } from 'vue-router'
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 { getRoutes } from './menu'
// 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()
// await getMenu()
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.VITE_BASE_URL), history: createWebHistory(import.meta.env.VITE_BASE_URL),
@@ -23,68 +19,41 @@ const router = createRouter({
}, },
{ {
path: '/:locale', path: '/:locale',
name: 'locale',
children: [ children: [
{ path: 'category/:category_id-:link_rewrite', component: () => import('@/views/CategoryView.vue'), name: 'category' },
{ path: 'products-datail', component: () => import('@/components/admin/ProductDetailView.vue'), name: 'product-detail' },
{ path: '', component: () => import('@/views/RepoChartView.vue'), name: 'home' },
{ path: 'products', component: () => import('@/components/admin/ProductsView.vue'), name: 'products' },
{ 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: 'customer-data', component: () => import('@/components/customer/PageCustomerData.vue'), name: 'customer-data' },
{ path: 'create-account', component: () => import('@/components/customer/PageCreateAccount.vue'), name: 'create-account' },
{ path: 'cart', component: () => import('@/components/customer/PageCart.vue'), name: 'cart' },
{ path: 'cart1', component: () => import('@/components/customer/Cart1.vue'), name: 'cart1' },
{ path: 'products-list', component: () => import('@/components/admin/PageProductsList.vue'), name: 'products-list' },
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true, } },
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'), component: () => import('@/views/NotFoundView.vue'),
name: 'not-found', name: 'not-found-child',
}, },
], ],
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFoundView.vue'), component: () => import('@/views/NotFoundView.vue'),
name: 'not-found', name: 'not-found-root',
}, },
], ],
}) })
// router.beforeEach((to, from, next) => { await getRoutes().then(routes => {
// const locale = to.params.locale as string const modules = import.meta.glob('/src/**/**/*.vue')
// const localeLang = langs.find((x) => x.iso_code == locale) routes.forEach(item => {
const component = modules[`/src${item.Component}`]
// if (locale && langs.length > 0) { if (!component) {
// const authStore = useAuthStore() console.error('Component not found:', item.Component)
// // console.log(authStore.isAuthenticated, to, from) return
// // if() }
// const validLocale = langs.find((l) => l.lang_code === locale)
// if (validLocale) { router.addRoute('locale', {
// currentLang.value = localeLang path: item.Path,
// if (!to.meta?.guest && !isAuthenticated()) { component,
// return next({ name: 'login', params: { locale } }) name: item.Name,
// } meta: item.Meta ? JSON.parse(item.Meta) : {}
})
// // return next() })
// return next() })
// } else if (locale) {
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
// }
// }
// if (!locale && to.path !== '/') {
// return next(`/${currentLang.value?.iso_code}${to.path}`)
// }
// next()
// })
router.beforeEach((to, from) => { router.beforeEach((to, from) => {
const locale = to.params.locale as string const locale = to.params.locale as string

View File

@@ -1,2 +1,8 @@
export * from '@types/lang' export * from '@types/lang'
export * from '@types/response' export * from '@types/response'
export interface ApiResponse {
message: string
items: Product[]
count: number
}

View File

@@ -43,32 +43,16 @@ CREATE TABLE IF NOT EXISTS b2b_top_menu (
INDEX FK_b2b_top_menu_parent_id_idx (parent_id ASC) INDEX FK_b2b_top_menu_parent_id_idx (parent_id ASC)
) ENGINE = InnoDB; ) ENGINE = InnoDB;
INSERT IGNORE INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`)
VALUES
-- ROOT
(1, JSON_COMPACT('{"name":"root","trans":{"pl":"Menu główne","en":"Main Menu","de":"Hauptmenü"}}'), NULL, '{}'),
-- LEVEL 1 INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES
(2, JSON_COMPACT('{"name":"dashboard","trans":{"pl":"Panel","en":"Dashboard","de":"Dashboard"}}'), 1, '{}'), (1, JSON_COMPACT('{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}'),NULL,'{}',1,1),
(3, JSON_COMPACT('{"name":"orders","trans":{"pl":"Zamówienia","en":"Orders","de":"Bestellungen"}}'), 1, '{}'), (3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,'{}',1,1),
(4, JSON_COMPACT('{"name":"customers","trans":{"pl":"Klienci","en":"Customers","de":"Kunden"}}'), 1, '{}'), (9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,'{}',1,1);
(5, JSON_COMPACT('{"name":"products","trans":{"pl":"Produkty","en":"Products","de":"Produkte"}}'), 1, '{}'),
(6, JSON_COMPACT('{"name":"reports","trans":{"pl":"Raporty","en":"Reports","de":"Berichte"}}'), 1, '{}'),
-- LEVEL 2 (Orders)
(7, JSON_COMPACT('{"name":"order_list","trans":{"pl":"Lista zamówień","en":"Order List","de":"Bestellliste"}}'), 3, '{}'),
(8, JSON_COMPACT('{"name":"pending_orders","trans":{"pl":"Oczekujące zamówienia","en":"Pending Orders","de":"Ausstehende Bestellungen"}}'), 3, '{}'),
(9, JSON_COMPACT('{"name":"carts","trans":{"pl":"Koszyki","en":"Carts","de":"Warenkörbe"}}'), 3, '{}'),
-- LEVEL 2 (Products)
(10, JSON_COMPACT('{"name":"product_list","trans":{"pl":"Lista produktów","en":"Product List","de":"Produktliste"}}'), 5, '{}'),
(11, JSON_COMPACT('{"name":"categories","trans":{"pl":"Kategorie","en":"Categories","de":"Kategorien"}}'), 5, '{}'),
(12, JSON_COMPACT('{"name":"inventory","trans":{"pl":"Magazyn","en":"Inventory","de":"Lagerbestand"}}'), 5, '{}'),
-- LEVEL 2 (Customers)
(13, JSON_COMPACT('{"name":"customer_list","trans":{"pl":"Lista klientów","en":"Customer List","de":"Kundenliste"}}'), 4, '{}'),
(14, JSON_COMPACT('{"name":"customer_groups","trans":{"pl":"Grupy klientów","en":"Customer Groups","de":"Kundengruppen"}}'), 4, '{}');
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_routes; DROP TABLE IF EXISTS b2b_routes;
DROP TABLE IF EXISTS b2b_top_menu;