expand_get_menu #42

Merged
goc_marek merged 5 commits from expand_get_menu into main 2026-03-31 14:55:36 +00:00
9 changed files with 179 additions and 38 deletions
Showing only changes of commit fa89723eb6 - Show all commits

View File

@@ -26,6 +26,7 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMenuHandler() handler := NewMenuHandler()
r.Get("/get-category-tree", handler.GetCategoryTree) r.Get("/get-category-tree", handler.GetCategoryTree)
r.Get("/get-breadcrumb", handler.GetBreadcrumb)
r.Get("/get-top-menu", handler.GetTopMenu) r.Get("/get-top-menu", handler.GetTopMenu)
return r return r
@@ -54,6 +55,36 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK)))
Review

you are responding with **model.Category is this intended?

you are responding with **model.Category is this intended?
} }
func (h *MenuHandler) GetBreadcrumb(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)))
}
root_category_id_attribute := c.Query("root_category_id")
root_category_id, err := strconv.Atoi(root_category_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
category_id_attribute := c.Query("category_id")
category_id, err := strconv.Atoi(category_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), 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(&breadcrumb, 0, i18n.T_(c, response.Message_OK)))
}
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint) lang_id, ok := c.Locals("langID").(uint)
if !ok { if !ok {

View File

@@ -21,10 +21,12 @@ type SettingsResponse struct {
// AppSettings represents app configuration // AppSettings represents app configuration
type AppSettings struct { type AppSettings struct {
Name string `json:"name"` Name string `json:"name"`
Environment string `json:"environment"` Environment string `json:"environment"`
BaseURL string `json:"base_url"` BaseURL string `json:"base_url"`
PasswordRegex string `json:"password_regex"` PasswordRegex string `json:"password_regex"`
CategoryTreeRootID uint `json:"category_tree_root_id"`
ShopDefaultLanguage uint `json:"shop_default_language"`
// Config config.Config `json:"config"` // Config config.Config `json:"config"`
} }
@@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
settings := SettingsResponse{ settings := SettingsResponse{
App: AppSettings{ App: AppSettings{
Name: cfg.App.Name, Name: cfg.App.Name,
Environment: cfg.App.Environment, Environment: cfg.App.Environment,
BaseURL: cfg.App.BaseURL, BaseURL: cfg.App.BaseURL,
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID,
ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE,
// Config: *config.Get(), // Config: *config.Get(),
}, },
Server: ServerSettings{ Server: ServerSettings{

33
app/model/category.go Normal file
View File

@@ -0,0 +1,33 @@
package model
type ScannedCategory struct {
CategoryID uint `gorm:"column:category_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"`
Visited bool //this is for internal backend use only
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategoryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategoryParams struct {
CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"`
}
type CategoryInBreadcrumb struct {
CategoryID uint `json:"category_id" form:"category_id"`
Name string `json:"name" form:"name"`
}

View File

@@ -84,30 +84,4 @@ type ProductFilters struct {
InStock uint `query:"stock,omitempty"` InStock uint `query:"stock,omitempty"`
} }
type ScannedCategory struct {
CategoryID uint `gorm:"column:category_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"`
Visited bool //this is for internal backend use only
}
type Category struct {
CategoryID uint `json:"category_id" form:"category_id"`
Label string `json:"label" form:"label"`
// Active bool `json:"active" form:"active"`
Params CategoryParams `json:"params" form:"params"`
Children []Category `json:"children" form:"children"`
}
type CategoryParams 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

@@ -1,6 +1,7 @@
package menuService package menuService
import ( import (
"slices"
"sort" "sort"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
@@ -112,6 +113,69 @@ 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) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
if err != nil {
return []model.CategoryInBreadcrumb{}, err
}
breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0
start_found := false
for i := 0; i < len(all_categories); i++ {
if all_categories[i].CategoryID == start_category_id {
start_index = i
start_found = true
break
}
}
if !start_found {
return []model.CategoryInBreadcrumb{}, responseErrors.ErrStartCategoryNotFound
}
// map category ids to indices
id_to_index := make(map[uint]int)
for i := 0; i < len(all_categories); i++ {
all_categories[i].Visited = false
id_to_index[all_categories[i].CategoryID] = i
}
// do a simple graph traversal, always jumping from node to its parent
index := start_index
success := true
for {
if all_categories[index].Visited {
success = false
break
}
all_categories[index].Visited = true
var next_category model.CategoryInBreadcrumb
next_category.CategoryID = all_categories[index].CategoryID
next_category.Name = all_categories[index].Name
breadcrumb = append(breadcrumb, next_category)
if all_categories[index].CategoryID == root_category_id {
break
}
next_index, ok := id_to_index[all_categories[index].ParentID]
if !ok {
success = false
break
}
index = next_index
}
slices.Reverse(breadcrumb)
if !success {
return breadcrumb, responseErrors.ErrRootNeverReached
}
return breadcrumb, nil
}
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 {

View File

@@ -3,6 +3,11 @@ 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 const SHOP_ID = 1
const SHOP_DEFAULT_LANGUAGE = 1
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
const CATEGORY_TREE_ROOT_ID = 2
const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart" const DEFAULT_NEW_CART_NAME = "new cart"

View File

@@ -50,8 +50,10 @@ var (
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 // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)") ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)")
ErrStartCategoryNotFound = errors.New("the start category has not been found")
ErrRootNeverReached = errors.New("the root category is not an ancestor of start category")
// Typed errors for carts handler // Typed errors for carts handler
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
@@ -148,6 +150,10 @@ func GetErrorCode(c fiber.Ctx, err error) string {
return i18n.T_(c, "error.no_root_found") return i18n.T_(c, "error.no_root_found")
case errors.Is(err, ErrCircularDependency): case errors.Is(err, ErrCircularDependency):
return i18n.T_(c, "error.circular_dependency") return i18n.T_(c, "error.circular_dependency")
case errors.Is(err, ErrStartCategoryNotFound):
return i18n.T_(c, "error.start_category_not_found")
case errors.Is(err, ErrRootNeverReached):
return i18n.T_(c, "error.root_never_reached")
case errors.Is(err, ErrMaxAmtOfCartsReached): case errors.Is(err, ErrMaxAmtOfCartsReached):
return i18n.T_(c, "error.max_amt_of_carts_reached") return i18n.T_(c, "error.max_amt_of_carts_reached")
@@ -193,6 +199,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrNoRootFound), errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrCircularDependency), errors.Is(err, ErrCircularDependency),
errors.Is(err, ErrStartCategoryNotFound),
errors.Is(err, ErrRootNeverReached),
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist): errors.Is(err, ErrProductOrItsVariationDoesNotExist):

View File

@@ -0,0 +1,22 @@
info:
name: get-breadcrumb
type: http
seq: 18
http:
method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13
params:
- name: root_category_id
value: "10"
type: query
- name: category_id
value: "13"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=3 url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=10
params: params:
- name: root_category_id - name: root_category_id
value: "3" value: "10"
type: query type: query
auth: inherit auth: inherit