From e0042524ced823d063c811e429aa3d9571295364 Mon Sep 17 00:00:00 2001 From: tsindt Date: Tue, 11 Aug 2026 21:41:44 +0200 Subject: [PATCH] ProductGroup Database API finished --- .../material-editor/src/data/product.ts | 0 .../material-editor/src/data/productGroup.ts | 13 ++ .../controller/productgroupsController.go | 112 ++++++++++++++++++ .../repositories/productgroupRepository.go | 78 +++++++++--- .../scpq-editor-server/server/httpServer.go | 10 ++ .../services/productgroupService.go | 40 +++++++ database/20260811_backup1.backup | Bin 0 -> 77223 bytes ...mp-simply_cpq_master_data-202608110844.sql | 0 ...mp-simply_cpq_master_data-202608110845.sql | 0 ...mp-simply_cpq_master_data-202608110848.sql | 0 database/product_groups.csv | 7 ++ database/property_types.csv | 8 ++ 12 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 buildtime/client/material-editor/src/data/product.ts create mode 100644 buildtime/client/material-editor/src/data/productGroup.ts create mode 100644 buildtime/server/src/scpq-editor-server/controller/productgroupsController.go create mode 100644 database/20260811_backup1.backup create mode 100644 database/dump-simply_cpq_master_data-202608110844.sql create mode 100644 database/dump-simply_cpq_master_data-202608110845.sql create mode 100644 database/dump-simply_cpq_master_data-202608110848.sql create mode 100644 database/product_groups.csv create mode 100644 database/property_types.csv diff --git a/buildtime/client/material-editor/src/data/product.ts b/buildtime/client/material-editor/src/data/product.ts new file mode 100644 index 000000000..e69de29bb diff --git a/buildtime/client/material-editor/src/data/productGroup.ts b/buildtime/client/material-editor/src/data/productGroup.ts new file mode 100644 index 000000000..326093d27 --- /dev/null +++ b/buildtime/client/material-editor/src/data/productGroup.ts @@ -0,0 +1,13 @@ +import type { PropertyType } from "./propertyType"; +import type { Product } from "./product"; + +export type ProductGroup = { + id: number; + label: string; + parentGroupId: number | null; + isVisible: boolean; + path: string; + SubGroups: ProductGroup[]; + PropertyTypes: PropertyType[]; + Products: Product[]; +}; \ No newline at end of file diff --git a/buildtime/server/src/scpq-editor-server/controller/productgroupsController.go b/buildtime/server/src/scpq-editor-server/controller/productgroupsController.go new file mode 100644 index 000000000..189884de8 --- /dev/null +++ b/buildtime/server/src/scpq-editor-server/controller/productgroupsController.go @@ -0,0 +1,112 @@ +package controller + +import ( + "encoding/json" + "io" + "log" + "net/http" + "scpq-editor-server/models" + "scpq-editor-server/services" + "strconv" + + "github.com/gin-gonic/gin" +) + +type ProductgroupsController struct { + ProductgroupService *services.ProductgroupService +} + +func NewProductgroupsController(ProductgroupService *services.ProductgroupService) *ProductgroupsController { + return &ProductgroupsController{ + ProductgroupService: ProductgroupService, + } +} + +func (lc ProductgroupsController) CreateProductgroup(ctx *gin.Context) { + + body, err := io.ReadAll(ctx.Request.Body) + if err != nil { + log.Println("Error while reading create Productgroup request body", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + var Productgroup models.Productgroup + err = json.Unmarshal(body, &Productgroup) + if err != nil { + log.Println("Error while unmarshaling create Productgroup request body", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + response, responseErr := lc.ProductgroupService.CreateProductgroup(&Productgroup) + if responseErr != nil { + ctx.AbortWithStatusJSON(responseErr.Status, responseErr) + return + } + ctx.JSON(http.StatusOK, response) +} + +func (lc ProductgroupsController) UpdateProductgroup(ctx *gin.Context) { + body, err := io.ReadAll(ctx.Request.Body) + if err != nil { + log.Println("Error while reading update Productgroup request body", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + var Productgroup models.Productgroup + err = json.Unmarshal(body, &Productgroup) + if err != nil { + log.Println("Error while unmarshaling update Productgroup request body", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + responseErr := lc.ProductgroupService.UpdateProductgroup(&Productgroup) + if responseErr != nil { + ctx.AbortWithStatusJSON(responseErr.Status, responseErr) + return + } + ctx.Status(http.StatusNoContent) +} + +func (lc ProductgroupsController) DeleteProductgroup(ctx *gin.Context) { + ProductgroupId := ctx.Param("id") + lId, err := strconv.Atoi(ProductgroupId) + if err != nil { + log.Println("Error ProductgroupId not int", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + responseErr := lc.ProductgroupService.DeleteProductgroup(lId) + if responseErr != nil { + ctx.JSON(responseErr.Status, responseErr) + return + } + ctx.Status(http.StatusNoContent) +} + +func (lc ProductgroupsController) GetProductgroup(ctx *gin.Context) { + ProductgroupId := ctx.Param("id") + lId, err := strconv.Atoi(ProductgroupId) + if err != nil { + log.Println("Error ProductgroupId not int", err) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + response, responseErr := lc.ProductgroupService.GetProductgroup(lId) + if responseErr != nil { + ctx.JSON(responseErr.Status, responseErr) + return + } + ctx.JSON(http.StatusOK, response) +} + +func (lc ProductgroupsController) GetAllProductgroups(ctx *gin.Context) { + response, responseErr := lc.ProductgroupService.GetAllProductgroups() + if responseErr != nil { + ctx.JSON(responseErr.Status, responseErr) + return + } + ctx.JSON(http.StatusOK, response) +} diff --git a/buildtime/server/src/scpq-editor-server/repositories/productgroupRepository.go b/buildtime/server/src/scpq-editor-server/repositories/productgroupRepository.go index 3594af70d..ab0490ab6 100644 --- a/buildtime/server/src/scpq-editor-server/repositories/productgroupRepository.go +++ b/buildtime/server/src/scpq-editor-server/repositories/productgroupRepository.go @@ -23,7 +23,7 @@ func NewProductgroupRepository(dbHandler *sql.DB, propertyTypesRepository *Prope } func (rr ProductgroupRepository) CreateProductgroup(Productgroup *models.Productgroup) (*models.Productgroup, *models.ResponseError) { - query := `INSERT INTO product_groups(label, parent_product_group_id, is_visible, path) VALUES($1,$2, $3, $4) RETURNING id` + query := `INSERT INTO product_groups(label, parent_group_id, is_visible, path) VALUES($1,$2, $3, $4) RETURNING id` rows, err := rr.dbHandler.Query(query, Productgroup.Label, Productgroup.ParentProductGroupID, Productgroup.IsVisible, Productgroup.Path) if err != nil { return nil, &models.ResponseError{ @@ -65,12 +65,17 @@ func (rr ProductgroupRepository) CreateProductgroup(Productgroup *models.Product Path: Productgroup.Path, } + addToParentErr := rr.addToParentProductGroup(pg) + if addToParentErr != nil { + return nil, addToParentErr + } + rr.m[pg.ID] = pg return pg, nil } func (rr ProductgroupRepository) UpdateProductgroup(Productgroup *models.Productgroup) *models.ResponseError { - query := `UPDATE product_groups SET label=$1, parent_product_group_id=$2, is_visible=$3, path=$4 WHERE id=$5` + query := `UPDATE product_groups SET label=$1, parent_group_id=$2, is_visible=$3, path=$4 WHERE id=$5` res, err := rr.dbHandler.Exec(query, Productgroup.Label, Productgroup.ParentProductGroupID, Productgroup.IsVisible, Productgroup.Path, Productgroup.ID) if err != nil { return &models.ResponseError{ @@ -96,20 +101,26 @@ func (rr ProductgroupRepository) UpdateProductgroup(Productgroup *models.Product return nil } -func findIndex(slice []*models.Productgroup, target models.Productgroup) int { - for i, v := range slice { - if v.ID == target.ID { - return i +func (rr ProductgroupRepository) DeleteProductgroup(ProductgroupId int) *models.ResponseError { + + // get sub product groups + subProductGroups, err2 := rr.GetSubProductgroups(ProductgroupId) + if err2 != nil { + return &models.ResponseError{ + Message: err2.Message, + Status: http.StatusInternalServerError, } } - return -1 -} + for _, spg := range subProductGroups { + rr.DeleteProductgroup(spg.ID) + } -func RemoveAt[T any](slice []T, index int) []T { - return append(slice[:index], slice[index+1:]...) -} + pg1, err2 := rr.GetProductgroup(ProductgroupId) + if pg1 != nil { + parentPG, _ := rr.GetProductgroup(pg1.ParentProductGroupID) + rr.RemoveProductGroupFromParentGroup(parentPG, pg1) + } -func (rr ProductgroupRepository) DeleteProductgroup(ProductgroupId int) *models.ResponseError { query := `DELETE FROM product_groups WHERE id=$1` res, err := rr.dbHandler.Exec(query, ProductgroupId) if err != nil { @@ -132,7 +143,7 @@ func (rr ProductgroupRepository) DeleteProductgroup(ProductgroupId int) *models. Status: http.StatusNotFound, } } - rr.m[ProductgroupId] = nil + delete(rr.m, ProductgroupId) return nil } @@ -142,7 +153,7 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr return pg, nil } - query := `SELECT id, label, parent_product_group_id, is_visible, path FROM product_groups WHERE id=$1` + query := `SELECT id, label, parent_group_id, is_visible, path FROM product_groups WHERE id=$1` rows, err := rr.dbHandler.Query(query, ProductgroupId) if err != nil { return nil, &models.ResponseError{ @@ -196,7 +207,7 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr subProductGroups, err2 := rr.GetSubProductgroups(pg.ID) if err2 != nil { return nil, &models.ResponseError{ - Message: err2.Error(), + Message: err2.Message, Status: http.StatusInternalServerError, } } @@ -208,7 +219,7 @@ func (rr ProductgroupRepository) GetProductgroup(ProductgroupId int) (*models.Pr propertyTypes, err3 := rr.GetPropertyTypesByProductGroupID(pg.ID) if err3 != nil { return nil, &models.ResponseError{ - Message: err3.Error(), + Message: err3.Message, Status: http.StatusInternalServerError, } } @@ -273,7 +284,7 @@ func (rr ProductgroupRepository) GetPropertyTypesByProductGroupID(productGroupID func (rr ProductgroupRepository) GetSubProductgroups(ProductgroupId int) ([]*models.Productgroup, *models.ResponseError) { var Productgroups []*models.Productgroup - query := `SELECT id FROM product_groups WHERE parent_product_group_id=$1` + query := `SELECT id FROM product_groups WHERE parent_group_id=$1` rows, err := rr.dbHandler.Query(query, ProductgroupId) if err != nil { @@ -372,6 +383,26 @@ func (rr ProductgroupRepository) GetAllProductgroups() ([]*models.Productgroup, return Productgroups, nil } +func (rr ProductgroupRepository) RemoveProductGroupFromParentGroup(parentProductGroup *models.Productgroup, childProductGroup *models.Productgroup) *models.ResponseError { + query := `UPDATE product_groups SET parent_group_id=NULL WHERE id=$1` + _, err := rr.dbHandler.Exec(query, childProductGroup.ID) + if err != nil { + return &models.ResponseError{ + Message: err.Error(), + Status: http.StatusInternalServerError, + } + } + // Remove child from parent's ChildProductGroups slice + for i, cpg := range parentProductGroup.ChildProductGroups { + if cpg.ID == childProductGroup.ID { + parentProductGroup.ChildProductGroups = append(parentProductGroup.ChildProductGroups[:i], parentProductGroup.ChildProductGroups[i+1:]...) + break + } + } + rr.m[parentProductGroup.ID] = parentProductGroup + return nil +} + func (rr ProductgroupRepository) addToParentProductGroup(ProductGroup *models.Productgroup) *models.ResponseError { Query := `UPDATE product_groups SET parent_group_id=$1 WHERE id=$2` _, err := rr.dbHandler.Exec(Query, ProductGroup.ParentProductGroupID, ProductGroup.ID) @@ -384,11 +415,22 @@ func (rr ProductgroupRepository) addToParentProductGroup(ProductGroup *models.Pr ParentProductGroup, err2 := rr.GetProductgroup(ProductGroup.ParentProductGroupID) if err2 != nil { return &models.ResponseError{ - Message: "Parent Productgroup not found with the given id " + strconv.Itoa(ProductGroup.ParentGroupID), + Message: "Parent Productgroup not found with the given id " + strconv.Itoa(ProductGroup.ParentProductGroupID), Status: http.StatusInternalServerError, } } ParentProductGroup.ChildProductGroups = append(ParentProductGroup.ChildProductGroups, *ProductGroup) + propertyTypes, err3 := rr.GetPropertyTypesByProductGroupID(ParentProductGroup.ID) + if err3 != nil { + return &models.ResponseError{ + Message: err3.Message, + Status: http.StatusInternalServerError, + } + } + for _, pt := range propertyTypes { + ProductGroup.PropertyTypes = append(ProductGroup.PropertyTypes, *pt) + } + rr.m[ParentProductGroup.ID] = ParentProductGroup return nil } diff --git a/buildtime/server/src/scpq-editor-server/server/httpServer.go b/buildtime/server/src/scpq-editor-server/server/httpServer.go index 540b217bb..c49941662 100644 --- a/buildtime/server/src/scpq-editor-server/server/httpServer.go +++ b/buildtime/server/src/scpq-editor-server/server/httpServer.go @@ -31,6 +31,7 @@ func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { valuecomplexitiesRepository := repositories.NewValuecomplexitiesRepository(dbHandler) translationsRepository := repositories.NewTranslationsRepository(dbHandler) propertytypesRepository := repositories.NewPropertyTypesRepository(dbHandler) + productgroupRepository := repositories.NewProductgroupRepository(dbHandler, propertytypesRepository) // service initialisation: languagesService := services.NewLanguagesService(languagesRepository) @@ -39,6 +40,7 @@ func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { valuetypesService := services.NewValuetypesService(valuetypesRepository) valuecomplexitiesService := services.NewValuecomplexitiesService(valuecomplexitiesRepository) translationsService := services.NewTranslationsService(translationsRepository) + productgroupService := services.NewProductgroupService(productgroupRepository) // controller initialisation: languageController := controller.NewLanguagesController(languagesService) @@ -47,6 +49,7 @@ func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { valuetypesController := controller.NewValuetypesController(valuetypesService) valuecomplexitiesController := controller.NewValuecomplexitiesController(valuecomplexitiesService) translationsController := controller.NewTranslationsController(translationsService) + productgroupsController := controller.NewProductgroupsController(productgroupService) // Service API router := gin.Default() @@ -67,6 +70,13 @@ func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { router.GET("/propertygroup/:id", propertygroupsController.GetPropertygroup) router.GET("/propertygroups", propertygroupsController.GetAllPropertygroups) + // productgroup centric routing + router.POST("/productgroup", productgroupsController.CreateProductgroup) + router.PUT("/productgroup", productgroupsController.UpdateProductgroup) + router.DELETE("/productgroup/:id", productgroupsController.DeleteProductgroup) + router.GET("/productgroup/:id", productgroupsController.GetProductgroup) + router.GET("/productgroups", productgroupsController.GetAllProductgroups) + // translation centric routing router.POST("/translation", translationsController.CreateTranslation) router.PUT("/translation", translationsController.UpdateTranslation) diff --git a/buildtime/server/src/scpq-editor-server/services/productgroupService.go b/buildtime/server/src/scpq-editor-server/services/productgroupService.go index 63afbae10..a76227103 100644 --- a/buildtime/server/src/scpq-editor-server/services/productgroupService.go +++ b/buildtime/server/src/scpq-editor-server/services/productgroupService.go @@ -1,6 +1,8 @@ package services import ( + "net/http" + "scpq-editor-server/models" "scpq-editor-server/repositories" ) @@ -13,3 +15,41 @@ func NewProductgroupService(ProductgroupRepository *repositories.ProductgroupRep ProductgroupRepository: ProductgroupRepository, } } + +func (ls ProductgroupService) CreateProductgroup(Productgroup *models.Productgroup) (*models.Productgroup, *models.ResponseError) { + responseErr := validateProductgroup(Productgroup) + if responseErr != nil { + return nil, responseErr + } + return ls.ProductgroupRepository.CreateProductgroup(Productgroup) +} + +func (ls ProductgroupService) UpdateProductgroup(Productgroup *models.Productgroup) *models.ResponseError { + responseErr := validateProductgroup(Productgroup) + if responseErr != nil { + return responseErr + } + return ls.ProductgroupRepository.UpdateProductgroup(Productgroup) +} + +func (ls ProductgroupService) DeleteProductgroup(ProductgroupId int) *models.ResponseError { + return ls.ProductgroupRepository.DeleteProductgroup(ProductgroupId) +} + +func (ls ProductgroupService) GetProductgroup(ProductgroupId int) (*models.Productgroup, *models.ResponseError) { + return ls.ProductgroupRepository.GetProductgroup(ProductgroupId) +} + +func (ls ProductgroupService) GetAllProductgroups() ([]*models.Productgroup, *models.ResponseError) { + return ls.ProductgroupRepository.GetAllProductgroups() +} + +func validateProductgroup(Productgroup *models.Productgroup) *models.ResponseError { + if Productgroup.Label == "" { + return &models.ResponseError{ + Message: "Invalid label", + Status: http.StatusBadRequest, + } + } + return nil +} diff --git a/database/20260811_backup1.backup b/database/20260811_backup1.backup new file mode 100644 index 0000000000000000000000000000000000000000..af43a664465f062c3ac1eb889423be238d9ea7c4 GIT binary patch literal 77223 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