From 8665c566ee35027455b74c18f78e4550fb89279c Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 10:52:36 +0200 Subject: [PATCH 1/7] added new category error, and some fixes --- app/delivery/web/api/restricted/menu.go | 18 ++++-- app/model/product.go | 6 +- app/repos/categoriesRepo/categoriesRepo.go | 11 ++-- .../productDescriptionRepo.go | 2 + app/service/menuService/menuService.go | 28 ++++++--- app/utils/responseErrors/responseErrors.go | 6 +- bo/components.d.ts | 57 +++++++++++++++++++ .../{get-menu.yml => get-category-tree.yml} | 8 +-- bruno/b2b-daniel/get-product-description.yml | 22 +++++++ 9 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 bo/components.d.ts rename bruno/b2b-daniel/{get-menu.yml => get-category-tree.yml} (52%) create mode 100644 bruno/b2b-daniel/get-product-description.yml diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index ee7e615..cee5673 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" @@ -23,25 +25,33 @@ func NewMenuHandler() *MenuHandler { func MenuHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMenuHandler() - r.Get("/get-menu", handler.GetMenu) + r.Get("/get-category-tree", handler.GetCategoryTree) r.Get("/get-top-menu", handler.GetTopMenu) return r } -func (h *MenuHandler) GetMenu(c fiber.Ctx) error { +func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error { lang_id, ok := c.Locals("langID").(uint) if !ok { return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - menu, err := h.menuService.GetMenu(lang_id) + + 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_tree, err := h.menuService.GetCategoryTree(uint(root_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(&menu, 0, i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK))) } func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { diff --git a/app/model/product.go b/app/model/product.go index 6a9212b..51a646b 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -93,16 +93,18 @@ type ScannedCategory struct { 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 CategpryParams `json:"params" form:"params"` + Params CategoryParams `json:"params" form:"params"` Children []Category `json:"children" form:"children"` } -type CategpryParams struct { +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"` diff --git a/app/repos/categoriesRepo/categoriesRepo.go b/app/repos/categoriesRepo/categoriesRepo.go index 45fdc2d..955292a 100644 --- a/app/repos/categoriesRepo/categoriesRepo.go +++ b/app/repos/categoriesRepo/categoriesRepo.go @@ -37,12 +37,11 @@ func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory, ps_category_lang.link_rewrite AS link_rewrite, ps_lang.iso_code AS iso_code `). - Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ? AND ??.id_lang = ?`, - categoryLangTbl, categoryLangTbl, categoryTbl, categoryLangTbl, constdata.SHOP_ID, categoryLangTbl, idLang). - Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ?`, - categoryShopTbl, categoryShopTbl, categoryTbl, categoryShopTbl, constdata.SHOP_ID). - Joins(`JOIN ? ON ??.id_lang = ??.id_lang`, - langTbl, langTbl, categoryLangTbl). + Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`, + constdata.SHOP_ID, idLang). + Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`, + constdata.SHOP_ID). + Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`). Scan(&allCategories).Error return allCategories, err diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index a5e7cbb..76463cd 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -28,6 +28,7 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid var ProductDescription model.ProductDescription err := db.Get(). + Model(dbmodel.PsProductLang{}). Where(&dbmodel.PsProductLang{ IDProduct: int32(productID), IDShop: int32(constdata.SHOP_ID), @@ -50,6 +51,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_ } err := db.Get(). + Model(dbmodel.PsProductLang{}). Where(&dbmodel.PsProductLang{ IDProduct: int32(productID), IDShop: int32(constdata.SHOP_ID), diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index e689ede..4e4b100 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -21,7 +21,7 @@ func New() *MenuService { } } -func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { +func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) { all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) if err != nil { return &model.Category{}, err @@ -31,7 +31,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { root_index := 0 root_found := false for i := 0; i < len(all_categories); i++ { - if all_categories[i].IsRoot == 1 { + if all_categories[i].CategoryID == root_category_id { root_index = i root_found = true break @@ -44,6 +44,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { // 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++ { + all_categories[i].Visited = false id_to_index[all_categories[i].CategoryID] = i } @@ -58,19 +59,32 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { } // finally, create the tree - tree := s.createTree(root_index, &all_categories, &children_indices) + tree, success := s.createTree(root_index, &all_categories, &children_indices) + if !success { + return &tree, responseErrors.ErrCircularDependency + } return &tree, nil } -func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category { +func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) (model.Category, bool) { node := s.scannedToNormalCategory((*all_categories)[index]) + if (*all_categories)[index].Visited { + return node, false + } + (*all_categories)[index].Visited = true + for i := 0; i < len((*children_indices)[index]); i++ { - node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)) + next_child, success := s.createTree((*children_indices)[index][i].Index, all_categories, children_indices) + if !success { + return node, false + } + node.Children = append(node.Children, next_child) } - return node + (*all_categories)[index].Visited = false // just in case we have a "diamond" diagram + return node, true } func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { @@ -83,7 +97,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod normal.CategoryID = scanned.CategoryID normal.Label = scanned.Name // normal.Active = scanned.Active == 1 - normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} + normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} normal.Children = []model.Category{} return normal } diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index f658430..2656a3b 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -50,7 +50,8 @@ var ( 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") + ErrNoRootFound = errors.New("no root found in categories table") + ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)") // Typed errors for carts handler ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") @@ -145,6 +146,8 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrNoRootFound): return i18n.T_(c, "error.no_root_found") + case errors.Is(err, ErrCircularDependency): + return i18n.T_(c, "error.circular_dependency") case errors.Is(err, ErrMaxAmtOfCartsReached): return i18n.T_(c, "error.max_amt_of_carts_reached") @@ -189,6 +192,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrBadPaging), errors.Is(err, ErrNoRootFound), + errors.Is(err, ErrCircularDependency), errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrProductOrItsVariationDoesNotExist): diff --git a/bo/components.d.ts b/bo/components.d.ts new file mode 100644 index 0000000..51b00ed --- /dev/null +++ b/bo/components.d.ts @@ -0,0 +1,57 @@ +/* eslint-disable */ +// @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 + +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] + CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] + CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default'] + Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.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_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default'] + LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default'] + PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] + PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default'] + PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default'] + PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default'] + PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default'] + PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default'] + PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.vue')['default'] + PageStatistic: typeof import('./src/components/customer/PageStatistic.vue')['default'] + Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default'] + Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default'] + ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default'] + ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default'] + ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default'] + TopBar: typeof import('./src/components/TopBar.vue')['default'] + TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default'] + UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default'] + UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default'] + UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default'] + UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] + UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default'] + UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default'] + UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default'] + UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default'] + UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] + UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default'] + UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] + UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default'] + UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] + USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] + USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] + UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default'] + } +} diff --git a/bruno/b2b-daniel/get-menu.yml b/bruno/b2b-daniel/get-category-tree.yml similarity index 52% rename from bruno/b2b-daniel/get-menu.yml rename to bruno/b2b-daniel/get-category-tree.yml index 959ec0e..6073e4f 100644 --- a/bruno/b2b-daniel/get-menu.yml +++ b/bruno/b2b-daniel/get-category-tree.yml @@ -1,14 +1,14 @@ info: - name: get-menu + name: get-category-tree type: http seq: 5 http: method: GET - url: http://localhost:3000/api/v1/restricted/menu/get-menu?lang_id=1 + url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=3 params: - - name: lang_id - value: "1" + - name: root_category_id + value: "3" type: query auth: inherit diff --git a/bruno/b2b-daniel/get-product-description.yml b/bruno/b2b-daniel/get-product-description.yml new file mode 100644 index 0000000..23e3c8e --- /dev/null +++ b/bruno/b2b-daniel/get-product-description.yml @@ -0,0 +1,22 @@ +info: + name: get-product-description + type: http + seq: 17 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=2 + params: + - name: productID + value: "51" + type: query + - name: productLangID + value: "2" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 From fa89723eb61ee31012f40003f11520e61617f2c5 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 11:40:57 +0200 Subject: [PATCH 2/7] add get-breadcrumb endpoint --- app/delivery/web/api/restricted/menu.go | 31 +++++++++++ app/delivery/web/api/settings.go | 20 ++++--- app/model/category.go | 33 +++++++++++ app/model/product.go | 26 --------- app/service/menuService/menuService.go | 64 ++++++++++++++++++++++ app/utils/const_data/consts.go | 5 ++ app/utils/responseErrors/responseErrors.go | 12 +++- bruno/b2b-daniel/get-breadcrumb.yml | 22 ++++++++ bruno/b2b-daniel/get-category-tree.yml | 4 +- 9 files changed, 179 insertions(+), 38 deletions(-) create mode 100644 app/model/category.go create mode 100644 bruno/b2b-daniel/get-breadcrumb.yml diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index cee5673..b269fb7 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -26,6 +26,7 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMenuHandler() r.Get("/get-category-tree", handler.GetCategoryTree) + r.Get("/get-breadcrumb", handler.GetBreadcrumb) r.Get("/get-top-menu", handler.GetTopMenu) 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))) } +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 { lang_id, ok := c.Locals("langID").(uint) if !ok { diff --git a/app/delivery/web/api/settings.go b/app/delivery/web/api/settings.go index eaa4acc..fb7af6c 100644 --- a/app/delivery/web/api/settings.go +++ b/app/delivery/web/api/settings.go @@ -21,10 +21,12 @@ type SettingsResponse struct { // AppSettings represents app configuration type AppSettings struct { - Name string `json:"name"` - Environment string `json:"environment"` - BaseURL string `json:"base_url"` - PasswordRegex string `json:"password_regex"` + Name string `json:"name"` + Environment string `json:"environment"` + BaseURL string `json:"base_url"` + PasswordRegex string `json:"password_regex"` + CategoryTreeRootID uint `json:"category_tree_root_id"` + ShopDefaultLanguage uint `json:"shop_default_language"` // Config config.Config `json:"config"` } @@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler { return func(c fiber.Ctx) error { settings := SettingsResponse{ App: AppSettings{ - Name: cfg.App.Name, - Environment: cfg.App.Environment, - BaseURL: cfg.App.BaseURL, - PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, + Name: cfg.App.Name, + Environment: cfg.App.Environment, + BaseURL: cfg.App.BaseURL, + PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, + CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID, + ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE, // Config: *config.Get(), }, Server: ServerSettings{ diff --git a/app/model/category.go b/app/model/category.go new file mode 100644 index 0000000..50e6ce9 --- /dev/null +++ b/app/model/category.go @@ -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"` +} diff --git a/app/model/product.go b/app/model/product.go index 51a646b..fa47790 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -84,30 +84,4 @@ type ProductFilters struct { 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 diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index 4e4b100..2d72cea 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -1,6 +1,7 @@ package menuService import ( + "slices" "sort" "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) 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) { items, err := s.routesRepo.GetTopMenu(id) if err != nil { diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 812364f..9c64ee5 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -3,6 +3,11 @@ package constdata // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` 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 DEFAULT_NEW_CART_NAME = "new cart" diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 2656a3b..c4247ea 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -50,8 +50,10 @@ var ( 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") - ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)") + ErrNoRootFound = errors.New("no root found in categories table") + 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 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") case errors.Is(err, ErrCircularDependency): 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): 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, ErrNoRootFound), errors.Is(err, ErrCircularDependency), + errors.Is(err, ErrStartCategoryNotFound), + errors.Is(err, ErrRootNeverReached), errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrProductOrItsVariationDoesNotExist): diff --git a/bruno/b2b-daniel/get-breadcrumb.yml b/bruno/b2b-daniel/get-breadcrumb.yml new file mode 100644 index 0000000..8b10c00 --- /dev/null +++ b/bruno/b2b-daniel/get-breadcrumb.yml @@ -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 diff --git a/bruno/b2b-daniel/get-category-tree.yml b/bruno/b2b-daniel/get-category-tree.yml index 6073e4f..c6b436e 100644 --- a/bruno/b2b-daniel/get-category-tree.yml +++ b/bruno/b2b-daniel/get-category-tree.yml @@ -5,10 +5,10 @@ info: http: 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: - name: root_category_id - value: "3" + value: "10" type: query auth: inherit From 1fa6206b75959c2ff647c20485abeb43220b96e0 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 12:00:30 +0200 Subject: [PATCH 3/7] update openapi and add the exists_in_database flag to get-product --- app/api/openapi.json | 199 +++++++++++++----- app/model/productDescription.go | 2 + .../productDescriptionRepo.go | 10 +- bruno/b2b-daniel/get-product-description.yml | 4 +- 4 files changed, 165 insertions(+), 50 deletions(-) diff --git a/app/api/openapi.json b/app/api/openapi.json index caf17fd..398bb8b 100644 --- a/app/api/openapi.json +++ b/app/api/openapi.json @@ -1127,21 +1127,32 @@ } } }, - "/api/v1/restricted/menu/get-menu": { + "/api/v1/restricted/menu/get-category-tree": { "get": { "tags": ["Menu"], - "summary": "Get menu structure", - "description": "Returns the menu structure for the current language. Requires authentication.", - "operationId": "getMenu", + "summary": "Get category tree", + "description": "Returns the category tree rooted at the given category ID for the current language. Requires authentication.", + "operationId": "getCategoryTree", "security": [ { "CookieAuth": [], "BearerAuth": [] } ], + "parameters": [ + { + "name": "root_category_id", + "in": "query", + "description": "Root category ID to build the tree from", + "required": true, + "schema": { + "type": "integer" + } + } + ], "responses": { "200": { - "description": "Menu retrieved successfully", + "description": "Category tree retrieved successfully", "content": { "application/json": { "schema": { @@ -1151,7 +1162,73 @@ } }, "400": { - "description": "Invalid request", + "description": "Invalid request or root category not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/menu/get-breadcrumb": { + "get": { + "tags": ["Menu"], + "summary": "Get breadcrumb", + "description": "Returns the breadcrumb path from the root category to the specified category for the current language. Requires authentication.", + "operationId": "getBreadcrumb", + "security": [ + { + "CookieAuth": [], + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "root_category_id", + "in": "query", + "description": "Root category ID (breadcrumb starting point)", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "category_id", + "in": "query", + "description": "Target category ID (breadcrumb destination)", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Breadcrumb retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request, category not found, or root never reached", "content": { "application/json": { "schema": { @@ -1221,7 +1298,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiResponse" + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/B2BTopMenu" + }, + "description": "Root menu items with nested children" + }, + "count": { + "type": "integer", + "description": "Number of root menu items" + } + } } } } @@ -1995,46 +2088,6 @@ } } }, - "MenuItem": { - "type": "object", - "description": "Menu item structure", - "properties": { - "category_id": { - "type": "integer", - "format": "uint", - "description": "Category ID" - }, - "label": { - "type": "string", - "description": "Menu item label" - }, - "params": { - "$ref": "#/components/schemas/MenuItemParams" - }, - "children": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MenuItem" - }, - "description": "Child menu items" - } - } - }, - "MenuItemParams": { - "type": "object", - "properties": { - "category_id": { - "type": "integer", - "format": "uint" - }, - "link_rewrite": { - "type": "string" - }, - "locale": { - "type": "string" - } - } - }, "Route": { "type": "object", "description": "Application route", @@ -2338,6 +2391,58 @@ "description": "Build date in RFC3339 format" } } + }, + "CategoryInBreadcrumb": { + "type": "object", + "description": "A single item in a category breadcrumb path", + "properties": { + "category_id": { + "type": "integer", + "format": "uint", + "description": "Category ID" + }, + "name": { + "type": "string", + "description": "Category name" + } + } + }, + "B2BTopMenu": { + "type": "object", + "description": "Top-level menu item for B2B back-office", + "properties": { + "menu_id": { + "type": "integer", + "description": "Menu item ID" + }, + "label": { + "type": "object", + "description": "Menu label as JSON (multilingual, e.g. {\"en\": \"Dashboard\", \"pl\": \"Panel\"})" + }, + "parent_id": { + "type": "integer", + "description": "Parent menu ID (null for root items)" + }, + "params": { + "type": "object", + "description": "Menu item parameters as JSON" + }, + "active": { + "type": "integer", + "description": "Active status (1 = active, 0 = inactive)" + }, + "position": { + "type": "integer", + "description": "Sort position" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/B2BTopMenu" + }, + "description": "Child menu items" + } + } } }, "securitySchemes": { diff --git a/app/model/productDescription.go b/app/model/productDescription.go index cb84fc8..3e7d5c1 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -19,6 +19,8 @@ type ProductDescription struct { DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"` DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"` Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"` + + ExistsInDatabse bool `gorm:"-" json:"exists_in_database"` } type ProductRow struct { diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index 76463cd..d569a75 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -1,6 +1,7 @@ package productDescriptionRepo import ( + "errors" "fmt" "git.ma-al.com/goc_daniel/b2b/app/db" @@ -8,6 +9,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" + "gorm.io/gorm" ) type UIProductDescriptionRepo interface { @@ -35,8 +37,14 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid IDLang: int32(productid_lang), }). First(&ProductDescription).Error - if err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + // handle "not found" case only + ProductDescription.ExistsInDatabse = false + } else if err != nil { return nil, fmt.Errorf("database error: %w", err) + } else { + ProductDescription.ExistsInDatabse = true } return &ProductDescription, nil diff --git a/bruno/b2b-daniel/get-product-description.yml b/bruno/b2b-daniel/get-product-description.yml index 23e3c8e..326b790 100644 --- a/bruno/b2b-daniel/get-product-description.yml +++ b/bruno/b2b-daniel/get-product-description.yml @@ -5,13 +5,13 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=2 + url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=4 params: - name: productID value: "51" type: query - name: productLangID - value: "2" + value: "4" type: query auth: inherit From a3f01eca7c5a54a3120804e28ba0a13954da483e Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 12:27:31 +0200 Subject: [PATCH 4/7] misspell fix --- app/model/productDescription.go | 2 +- app/repos/productDescriptionRepo/productDescriptionRepo.go | 4 ++-- bruno/b2b-daniel/get-product-description.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/model/productDescription.go b/app/model/productDescription.go index 3e7d5c1..4781be5 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -20,7 +20,7 @@ type ProductDescription struct { DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"` Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"` - ExistsInDatabse bool `gorm:"-" json:"exists_in_database"` + ExistsInDatabase bool `gorm:"-" json:"exists_in_database"` } type ProductRow struct { diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index d569a75..1b0faf8 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -40,11 +40,11 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid if errors.Is(err, gorm.ErrRecordNotFound) { // handle "not found" case only - ProductDescription.ExistsInDatabse = false + ProductDescription.ExistsInDatabase = false } else if err != nil { return nil, fmt.Errorf("database error: %w", err) } else { - ProductDescription.ExistsInDatabse = true + ProductDescription.ExistsInDatabase = true } return &ProductDescription, nil diff --git a/bruno/b2b-daniel/get-product-description.yml b/bruno/b2b-daniel/get-product-description.yml index 326b790..63a7447 100644 --- a/bruno/b2b-daniel/get-product-description.yml +++ b/bruno/b2b-daniel/get-product-description.yml @@ -5,13 +5,13 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=4 + url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=1 params: - name: productID value: "51" type: query - name: productLangID - value: "4" + value: "1" type: query auth: inherit From fb4f7048ab613e6e10a4c84c2f46c6574cab0dee Mon Sep 17 00:00:00 2001 From: Arina Yakovenko Date: Tue, 31 Mar 2026 12:22:21 +0200 Subject: [PATCH 5/7] fix: requests --- bo/src/components/admin/PageProducts.vue | 100 ++++---- bo/src/components/admin/ProductDetailView.vue | 228 +++++++++--------- bo/src/components/customer/PageProducts.vue | 2 +- bo/src/router/menu.ts | 11 +- bo/src/router/settings.ts | 2 + bo/src/stores/product.ts | 2 +- bo/src/types/settings.d.ts | 1 + 7 files changed, 183 insertions(+), 163 deletions(-) diff --git a/bo/src/components/admin/PageProducts.vue b/bo/src/components/admin/PageProducts.vue index 09cbe53..46cb4ae 100644 --- a/bo/src/components/admin/PageProducts.vue +++ b/bo/src/components/admin/PageProducts.vue @@ -1,15 +1,20 @@