diff --git a/controllers/productController.go b/controllers/productController.go index 1e3d014..4c07482 100644 --- a/controllers/productController.go +++ b/controllers/productController.go @@ -535,10 +535,10 @@ func (ctl *ProductController) ImportCatalogueProduct(c *fiber.Ctx) error { } for _, req := range data { - if req.Tenantid == 0 || req.Locationid == 0 || req.Brand == "" || req.Catalogueid == 0 || req.Categoryid == 0 || req.Subcategoryid == 0 { + if req.Tenantid == 0 || req.Locationid == 0 || req.Brand == "" || req.Catalogueid == 0 || req.Categoryid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, - "message": "tenantid, locationid, brand, catalogueid, categoryid, and subcategoryid are required", + "message": "tenantid, locationid, brand, catalogueid, and categoryid are required", "status": false, }) } @@ -590,6 +590,33 @@ func (ctl *ProductController) GetImportedCatalogueProducts(c *fiber.Ctx) error { }) } +func (ctl *ProductController) GetTenantCategories(c *fiber.Ctx) error { + tenantID, _ := strconv.Atoi(c.Query("tenantid")) + if tenantID == 0 { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "tenantid is required", + "status": false, + }) + } + + categories, err := ctl.productService.GetTenantCategories(tenantID) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "message": "Success", + "status": true, + "details": categories, + }) +} + func (ctl *ProductController) DeleteProductLocation(c *fiber.Ctx) error { var input struct { Tenantid int `json:"tenantid"` diff --git a/models/product.go b/models/product.go index e34b22c..80da380 100644 --- a/models/product.go +++ b/models/product.go @@ -256,6 +256,15 @@ type Subcategory struct { Image string `json:"image" gorm:"column:image"` } +// TenantCategory is a categoryid actually in use by a tenant's own products, +// with a best-effort name. Used instead of the global productcategories list +// for the import category picker, since that master table is missing rows +// for categoryids that are nonetheless in real use (e.g. categoryid 2). +type TenantCategory struct { + Categoryid int `json:"categoryid"` + Categoryname string `json:"categoryname"` +} + // ImportedCatalogueRef identifies a catalogue product a tenant has already // imported. Brand is always included, even when a caller filtered by a // single brand, because a bare catalogueid is ambiguous across brand tables. diff --git a/repositories/productRepository.go b/repositories/productRepository.go index d15a663..7b892e5 100644 --- a/repositories/productRepository.go +++ b/repositories/productRepository.go @@ -40,6 +40,7 @@ type ProductRepository interface { FindTenantProductByCatalogueRef(tenantid int, brand string, catalogueid int64) (*models.Products, error) CreateProductReturningID(product models.Products) (int, error) GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) + GetTenantCategories(tenantid int) ([]models.TenantCategory, error) UpdateProductPricing(productid int, retailprice, productcost, taxpercent float64) error } @@ -56,10 +57,18 @@ func (r *productRepository) GetProductSubCategory(categoryID, tenantID int) ([]m var query strings.Builder var args []interface{} - query.WriteString("SELECT * FROM productsubcategories WHERE 1=1") + // tenantid is selected via COALESCE (not SELECT *) because the relaxed + // filter below can now return rows where it's NULL, which won't scan + // into the model's non-pointer int field otherwise. + query.WriteString(`SELECT subcatid, categoryid, COALESCE(tenantid, 0) AS tenantid, + subcatname, image, status, sortorder, createdby, created, updated + FROM productsubcategories WHERE 1=1`) if tenantID != 0 { - query.WriteString(" AND tenantid = ?") + // Some subcategories are tenant-owned overrides, others are shared + // master data with no tenant attached (tenantid NULL/0) — match both + // so a tenant sees the global set in addition to their own. + query.WriteString(" AND (tenantid = ? OR tenantid IS NULL OR tenantid = 0)") args = append(args, tenantID) } if categoryID != 0 { @@ -804,6 +813,26 @@ func (r *productRepository) GetImportedCatalogueRefs(tenantid int, brand string) return refs, err } +// GetTenantCategories returns the distinct categoryids this tenant's own +// products actually use, LEFT JOINed against productcategories for a name +// (falling back to a synthesized label when that master row is missing — +// it's incomplete in practice, e.g. categoryid 2 has no productcategories +// row despite being in real use). This is deliberately not the global +// productcategories list: that list can omit categoryids tenants actually +// have products in, which would make the import category picker unusable. +func (r *productRepository) GetTenantCategories(tenantid int) ([]models.TenantCategory, error) { + categories := make([]models.TenantCategory, 0) + err := r.db.Raw(` + SELECT DISTINCT p.categoryid, + COALESCE(NULLIF(pc.categoryname, ''), 'Category ' || p.categoryid) AS categoryname + FROM products p + LEFT JOIN productcategories pc ON pc.categoryid = p.categoryid + WHERE p.tenantid = ? AND p.categoryid != 0 + ORDER BY categoryname + `, tenantid).Scan(&categories).Error + return categories, err +} + // UpdateProductPricing updates only the pricing fields on a product // snapshot, used when a catalogue product is re-imported with new pricing. func (r *productRepository) UpdateProductPricing(productid int, retailprice, productcost, taxpercent float64) error { diff --git a/routes/productroutes.go b/routes/productroutes.go index 07585c3..40986a7 100644 --- a/routes/productroutes.go +++ b/routes/productroutes.go @@ -28,6 +28,7 @@ func RegisterProductRoutes(api fiber.Router, f *facade.Facade) { products.Post("/createproductlocation", f.ProductController.CreateProductLocation) products.Post("/importcatalogueproduct", f.ProductController.ImportCatalogueProduct) products.Get("/getimportedcatalogueproducts", f.ProductController.GetImportedCatalogueProducts) + products.Get("/gettenantcategories", f.ProductController.GetTenantCategories) products.Delete("/deleteproductlocation", f.ProductController.DeleteProductLocation) products.Post("/createproductvariant", f.ProductController.CreateProductVariant) @@ -46,5 +47,6 @@ func RegisterProductRoutes(api fiber.Router, f *facade.Facade) { products.Put("/updateproductlocation", f.ProductController.UpdateProductLocation) products.Post("/importcatalogueproduct", f.ProductController.ImportCatalogueProduct) products.Get("/getimportedcatalogueproducts", f.ProductController.GetImportedCatalogueProducts) + products.Get("/gettenantcategories", f.ProductController.GetTenantCategories) } diff --git a/services/productService.go b/services/productService.go index 800e47f..7f50c18 100644 --- a/services/productService.go +++ b/services/productService.go @@ -31,6 +31,7 @@ type ProductService interface { DeleteProductLocation(tenantid, locationid, productid int) error ImportCatalogueProduct(reqs []models.ImportCatalogueProductRequest) error GetImportedCatalogueRefs(tenantid int, brand string) ([]models.ImportedCatalogueRef, error) + GetTenantCategories(tenantid int) ([]models.TenantCategory, error) } type productService struct { repo repositories.ProductRepository @@ -288,3 +289,7 @@ func (s *productService) GetImportedCatalogueRefs(tenantid int, brand string) ([ return s.repo.GetImportedCatalogueRefs(tenantid, brand) } +func (s *productService) GetTenantCategories(tenantid int) ([]models.TenantCategory, error) { + return s.repo.GetTenantCategories(tenantid) +} +