From b67c4e3aefcf77f7ba1657dce044de228b5a51b7 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 20 Mar 2026 12:38:41 +0100 Subject: [PATCH] endpoint returning tree of categories --- .gitignore | 3 +- app/delivery/web/api/restricted/menu.go | 18 +++- app/model/product.go | 15 ++++ app/service/menuService/menuService.go | 98 +++++++++++++++------ app/utils/responseErrors/responseErrors.go | 9 +- repository/categoriesRepo/categoriesRepo.go | 31 +++++++ 6 files changed, 144 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index d5394cc..0408331 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ assets/public/dist bin/ i18n/*.json *_templ.go -tmp/main \ No newline at end of file +tmp/main +test.go \ No newline at end of file diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index 46fb0f9..37c2596 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -1,6 +1,8 @@ package restricted import ( + "strconv" + "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" @@ -29,7 +31,21 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { } func (h *MenuHandler) GetMenu(c fiber.Ctx) error { - menu, err := h.menuService.GetMenu() + + 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")) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + menu, err := h.menuService.GetMenu(uint(id_shop), uint(id_lang)) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/model/product.go b/app/model/product.go index 4a1436a..eb63f68 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -81,4 +81,19 @@ type ProductFilters struct { InStock uint `query:"stock,omitempty"` } +type ScannedCategory struct { + CategoryID uint `gorm:"column:ID;primaryKey"` + Name string `gorm:"column:name"` + Active uint `gorm:"column:active"` + Position uint `gorm:"column:position"` + ParentID uint `gorm:"column:id_parent"` + IsRoot uint `gorm:"column:is_root_category"` +} +type Category struct { + CategoryID uint `json:"category_id" form:"category_id"` + Name string `json:"name" form:"name"` + Active uint `json:"active" form:"active"` + Subcategories []Category `json:"subcategories" form:"subcategories"` +} + type FeatVal = map[uint][]uint diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index 420704d..3186233 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -1,6 +1,12 @@ package menuService -import "git.ma-al.com/goc_daniel/b2b/repository/categoriesRepo" +import ( + "sort" + + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "git.ma-al.com/goc_daniel/b2b/repository/categoriesRepo" +) type MenuService struct { categoriesRepo categoriesRepo.UICategoriesRepo @@ -12,35 +18,73 @@ func New() *MenuService { } } -func (s *MenuService) GetMenu() (string, error) { - // var menu_json string +func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error) { + all_categories, err := s.categoriesRepo.GetAllCategories(id_shop, id_lang) + if err != nil { + return model.Category{}, err + } - // products, err := s.listProductsRepo.GetListing(id_shop, id_lang, p, filters) - // if err != nil { - // return products, 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 + } - // var loopErr error - // parallel.ForEach(products.Items, func(t model.Product, i int) { - // // products.Items[i].PriceTaxed *= currRate.Rate.InexactFloat64() - // // products.Items[i].PriceTaxed = tiny_util.RoundUpMonetary(products.Items[i].PriceTaxed) + // 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 + } - // if products.Items[i].Name.IsNull() { - // translation, err := s.listProductsRepo.GetTranslation(ctx, products.Items[i].ID, defaults.DefaultLanguageID) - // if err != nil { - // loopErr = err - // return - // } - // products.Items[i].Name = nullable.FromPrimitiveString(translation.Name) - // products.Items[i].DescriptionShort = nullable.FromPrimitiveString(translation.DescriptionShort) - // products.Items[i].LinkRewrite = nullable.FromPrimitiveString(translation.LinkRewrite) - // } - // }) - // if loopErr != nil { - // return products, errs.Handled(span, loopErr, errs.InternalError, errs.ERR_TODO) - // } + 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}) + } - // return products, nil + for key := range children_indices { + sort.Sort(ByPosition(children_indices[key])) + } - return "", nil + // 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.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)) + } + + return node +} + +func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { + var normal model.Category + normal.Active = scanned.Active + normal.CategoryID = scanned.CategoryID + normal.Name = scanned.Name + normal.Subcategories = []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 } diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index da44d11..fffa66b 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -48,6 +48,9 @@ var ( // Typed errors for product list handler ErrBadPaging = errors.New("bad or missing paging attribute value in header") + + // Typed errors for menu handler + ErrNoRootFound = errors.New("no root found in categories table") ) // Error represents an error with HTTP status code @@ -135,6 +138,9 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrBadPaging): return i18n.T_(c, "error.err_bad_paging") + case errors.Is(err, ErrNoRootFound): + return i18n.T_(c, "error.no_root_found") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -169,7 +175,8 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrBadAttribute), errors.Is(err, ErrBadField), errors.Is(err, ErrInvalidXHTML), - errors.Is(err, ErrBadPaging): + errors.Is(err, ErrBadPaging), + errors.Is(err, ErrNoRootFound): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/repository/categoriesRepo/categoriesRepo.go b/repository/categoriesRepo/categoriesRepo.go index 56625c0..4303844 100644 --- a/repository/categoriesRepo/categoriesRepo.go +++ b/repository/categoriesRepo/categoriesRepo.go @@ -1,6 +1,12 @@ package categoriesRepo +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" +) + type UICategoriesRepo interface { + GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error) } type CategoriesRepo struct{} @@ -8,3 +14,28 @@ type CategoriesRepo struct{} func New() UICategoriesRepo { return &CategoriesRepo{} } + +func (repo *CategoriesRepo) GetAllCategories(id_shop uint, 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 + 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 = ?`, + id_shop, id_lang, id_shop). + Scan(&allCategories).Error + + return allCategories, err +}