diff --git a/.env b/.env index 8fe69fd..6dcd355 100644 --- a/.env +++ b/.env @@ -25,6 +25,9 @@ AUTH_REFRESH_EXPIRATION=604800 MEILISEARCH_URL=http://localhost:7700 MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +# OpenAI +OPENAI_KEY=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 + # Google Translate Client GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json GOOGLE_CLOUD_PROJECT_ID=translation-343517 @@ -33,6 +36,7 @@ GOOGLE_CLOUD_PROJECT_ID=translation-343517 OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com OAUTH_GOOGLE_CLIENT_SECRET=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 OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback + # Email Configuration (SMTP) # Set EMAIL_ENABLED=true to require email verification EMAIL_ENABLED=true @@ -51,3 +55,5 @@ PDF_SERVER_URL=http://localhost:8000 FILE_MAAL_PL_USER=git_operator FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1 + +IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta \ No newline at end of file diff --git a/app/api/openapi.json b/app/api/openapi.json index f3715cb..fd09b1c 100644 --- a/app/api/openapi.json +++ b/app/api/openapi.json @@ -28,6 +28,26 @@ "name": "Languages", "description": "Language and translation endpoints" }, + { + "name": "Products", + "description": "Product listing and description endpoints (under /api/v1/restricted, requires authentication)" + }, + { + "name": "Product Description", + "description": "Product description management and translation endpoints (under /api/v1/restricted/product-description, requires authentication)" + }, + { + "name": "Menu", + "description": "Menu and routing endpoints (under /api/v1/restricted/menu, requires authentication)" + }, + { + "name": "Search", + "description": "MeiliSearch endpoints (under /api/v1/restricted/meili-search, requires authentication)" + }, + { + "name": "Locale", + "description": "Locale selection endpoints (under /api/v1/restricted/langs-and-countries, requires authentication)" + }, { "name": "Repo", "description": "Repository time tracking data endpoints (under /api/v1/restricted/repo, requires authentication)" @@ -1018,6 +1038,670 @@ } } } + }, + "/api/v1/restricted/list-products/get-listing": { + "get": { + "tags": ["Products"], + "summary": "Get product listing", + "description": "Returns a paginated list of products with their basic information. Requires authentication.", + "operationId": "getProductListing", + "security": [ + { + "CookieAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number (1-based)", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page", + "required": false, + "schema": { + "type": "integer", + "default": 30 + } + } + ], + "responses": { + "200": { + "description": "Product listing retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/product-description/get-product-description": { + "get": { + "tags": ["Product Description"], + "summary": "Get product description", + "description": "Returns the product description for a given product ID and language. Requires authentication.", + "operationId": "getProductDescription", + "security": [ + { + "CookieAuth": [] + } + ], + "parameters": [ + { + "name": "productID", + "in": "query", + "description": "Product ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "productLangID", + "in": "query", + "description": "Language ID for the product description", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + } + ], + "responses": { + "200": { + "description": "Product description retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/product-description/save-product-description": { + "post": { + "tags": ["Product Description"], + "summary": "Save product description", + "description": "Saves the product description for a given product ID in the specified language. Requires authentication.", + "operationId": "saveProductDescription", + "security": [ + { + "CookieAuth": [] + } + ], + "parameters": [ + { + "name": "productID", + "in": "query", + "description": "Product ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "productLangID", + "in": "query", + "description": "Language ID for the product description", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductDescriptionUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Product description saved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request parameters or body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/product-description/translate-product-description": { + "get": { + "tags": ["Product Description"], + "summary": "Translate product description", + "description": "Translates the product description from one language to another using AI (OpenAI or Google). Requires authentication.", + "operationId": "translateProductDescription", + "security": [ + { + "CookieAuth": [] + } + ], + "parameters": [ + { + "name": "productID", + "in": "query", + "description": "Product ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "productFromLangID", + "in": "query", + "description": "Source language ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "productToLangID", + "in": "query", + "description": "Target language ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "model", + "in": "query", + "description": "AI model to use for translation (OpenAI or Google)", + "required": true, + "schema": { + "type": "string", + "enum": ["OpenAI", "Google"] + } + } + ], + "responses": { + "200": { + "description": "Product description translated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request parameters", + "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-menu": { + "get": { + "tags": ["Menu"], + "summary": "Get menu structure", + "description": "Returns the menu structure for the current language. Requires authentication.", + "operationId": "getMenu", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Menu retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "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-routes": { + "get": { + "tags": ["Menu"], + "summary": "Get routes", + "description": "Returns the routing structure for the current language. Requires authentication.", + "operationId": "getRoutes", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Routes retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/meili-search/search": { + "get": { + "tags": ["Search"], + "summary": "Search products", + "description": "Searches products using MeiliSearch. Requires authentication.", + "operationId": "searchProducts", + "security": [ + { + "CookieAuth": [] + } + ], + "parameters": [ + { + "name": "query", + "in": "query", + "description": "Search query string", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of results", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "id_category", + "in": "query", + "description": "Filter by category ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint" + } + }, + { + "name": "price_lower_bound", + "in": "query", + "description": "Lower price bound", + "required": true, + "schema": { + "type": "number", + "format": "double" + } + }, + { + "name": "price_upper_bound", + "in": "query", + "description": "Upper price bound", + "required": true, + "schema": { + "type": "number", + "format": "double" + } + } + ], + "responses": { + "200": { + "description": "Search results retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/meili-search/create-index": { + "get": { + "tags": ["Search"], + "summary": "Create search index", + "description": "Creates a MeiliSearch index for products. Requires superadmin access.", + "operationId": "createSearchIndex", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Index created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/meili-search/test": { + "get": { + "tags": ["Search"], + "summary": "Test MeiliSearch", + "description": "Tests the MeiliSearch connection. Requires superadmin access.", + "operationId": "testMeiliSearch", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "MeiliSearch test successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/langs-and-countries/get-languages": { + "get": { + "tags": ["Locale"], + "summary": "Get available languages", + "description": "Returns a list of available languages for the application. Requires authentication.", + "operationId": "getAvailableLanguages", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Languages retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/restricted/langs-and-countries/get-countries": { + "get": { + "tags": ["Locale"], + "summary": "Get countries and currencies", + "description": "Returns a list of countries with their associated currencies. Requires authentication.", + "operationId": "getCountriesAndCurrencies", + "security": [ + { + "CookieAuth": [] + } + ], + "responses": { + "200": { + "description": "Countries and currencies retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Not authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } } }, "components": { @@ -1163,6 +1847,272 @@ } } }, + "ApiResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Response message" + }, + "items": { + "type": "object", + "description": "Response data" + }, + "count": { + "type": "integer", + "description": "Number of items returned" + } + } + }, + "ProductDescriptionUpdate": { + "type": "object", + "description": "Map of fields to update for product description", + "additionalProperties": { + "type": "string" + }, + "example": { + "name": "Product Name", + "description": "

Product description in HTML

", + "description_short": "

Short description

", + "meta_title": "Meta Title", + "meta_description": "Meta description text", + "available_now": "In Stock", + "available_later": "Out of Stock", + "usage": "

Usage instructions

" + } + }, + "ProductDescription": { + "type": "object", + "description": "Product description in a specific language", + "properties": { + "product_id": { + "type": "integer", + "format": "uint", + "description": "Product ID" + }, + "shop_id": { + "type": "integer", + "format": "uint", + "description": "Shop ID" + }, + "lang_id": { + "type": "integer", + "format": "uint", + "description": "Language ID" + }, + "name": { + "type": "string", + "description": "Product name" + }, + "description": { + "type": "string", + "description": "Full product description (HTML)" + }, + "description_short": { + "type": "string", + "description": "Short product description (HTML)" + }, + "link_rewrite": { + "type": "string", + "description": "URL-friendly slug" + }, + "meta_description": { + "type": "string", + "description": "Meta description" + }, + "meta_keywords": { + "type": "string", + "description": "Meta keywords" + }, + "meta_title": { + "type": "string", + "description": "Meta title" + }, + "available_now": { + "type": "string", + "description": "Text shown when item is in stock" + }, + "available_later": { + "type": "string", + "description": "Text shown when item is out of stock" + }, + "delivery_in_stock": { + "type": "string", + "description": "Delivery in stock text" + }, + "delivery_out_stock": { + "type": "string", + "description": "Delivery out of stock text" + }, + "usage": { + "type": "string", + "description": "Usage instructions (HTML)" + } + } + }, + "Country": { + "type": "object", + "description": "Country with its currency", + "properties": { + "id": { + "type": "integer", + "format": "uint", + "description": "Country ID" + }, + "name": { + "type": "string", + "description": "Country name" + }, + "flag": { + "type": "string", + "description": "Flag emoji or code" + }, + "currency_id": { + "type": "integer", + "format": "uint", + "description": "Currency ID" + }, + "currency_iso_code": { + "type": "string", + "description": "Currency ISO code (e.g., EUR, USD)" + }, + "currency_name": { + "type": "string", + "description": "Currency name" + } + } + }, + "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", + "properties": { + "id": { + "type": "integer", + "format": "uint", + "description": "Route ID" + }, + "name": { + "type": "string", + "description": "Route name" + }, + "path": { + "type": "string", + "description": "Route path" + }, + "component": { + "type": "string", + "description": "Vue component path" + }, + "layout": { + "type": "string", + "description": "Layout type" + }, + "meta": { + "type": "object", + "description": "Route metadata" + }, + "is_active": { + "type": "boolean", + "description": "Whether the route is active" + }, + "sort_order": { + "type": "integer", + "description": "Sort order" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + }, + "description": "Child routes" + } + } + }, + "MeiliSearchResult": { + "type": "object", + "description": "MeiliSearch product result", + "properties": { + "id_product": { + "type": "integer", + "format": "uint", + "description": "Product ID" + }, + "name": { + "type": "string", + "description": "Product name" + }, + "active": { + "type": "integer", + "description": "Active status" + }, + "price": { + "type": "number", + "format": "double", + "description": "Product price" + }, + "description": { + "type": "string", + "description": "Product description" + }, + "description_short": { + "type": "string", + "description": "Short description" + }, + "reference": { + "type": "string", + "description": "Product reference" + }, + "id_category": { + "type": "integer", + "format": "uint", + "description": "Category ID" + }, + "category_name": { + "type": "string", + "description": "Category name" + } + } + }, "Language": { "type": "object", "properties": { diff --git a/app/delivery/web/api/restricted/langsAndCountries.go b/app/delivery/web/api/restricted/langsAndCountries.go deleted file mode 100644 index dfd6aad..0000000 --- a/app/delivery/web/api/restricted/langsAndCountries.go +++ /dev/null @@ -1,52 +0,0 @@ -package restricted - -import ( - "git.ma-al.com/goc_daniel/b2b/app/service/langsAndCountriesService" - "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" - "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" - "git.ma-al.com/goc_daniel/b2b/app/utils/response" - "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" - "github.com/gofiber/fiber/v3" -) - -// LangsAndCountriesHandler for getting languages and countries data -type LangsAndCountriesHandler struct { - langsAndCountriesService *langsAndCountriesService.LangsAndCountriesService -} - -// NewLangsAndCountriesHandler creates a new LangsAndCountriesHandler instance -func NewLangsAndCountriesHandler() *LangsAndCountriesHandler { - langsAndCountriesService := langsAndCountriesService.New() - return &LangsAndCountriesHandler{ - langsAndCountriesService: langsAndCountriesService, - } -} - -func LangsAndCountriesHandlerRoutes(r fiber.Router) fiber.Router { - handler := NewLangsAndCountriesHandler() - - r.Get("/get-languages", handler.GetLanguages) - r.Get("/get-countries", handler.GetCountries) - - return r -} - -func (h *LangsAndCountriesHandler) GetLanguages(c fiber.Ctx) error { - languages, err := h.langsAndCountriesService.GetLanguages() - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK))) -} - -func (h *LangsAndCountriesHandler) GetCountries(c fiber.Ctx) error { - countries, err := h.langsAndCountriesService.GetCountriesAndCurrencies() - if err != nil { - return c.Status(responseErrors.GetErrorStatus(err)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) - } - - return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK))) -} diff --git a/app/delivery/web/api/restricted/listProducts.go b/app/delivery/web/api/restricted/listProducts.go index 971a802..85317a6 100644 --- a/app/delivery/web/api/restricted/listProducts.go +++ b/app/delivery/web/api/restricted/listProducts.go @@ -1,8 +1,6 @@ package restricted import ( - "strconv" - "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/service/listProductsService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" @@ -52,20 +50,13 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error { // "override_currency": c.Query("override_currency", ""), // } - id_shop_attribute := c.Query("shopID") - id_shop, err := strconv.Atoi(id_shop_attribute) - if err != nil { + id_lang, 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))) } - 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))) - } - - listing, err := h.listProductsService.GetListing(uint(id_shop), uint(id_lang), paging, filters) + listing, err := h.listProductsService.GetListing(id_lang, paging, filters) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/delivery/web/api/restricted/localeSelector.go b/app/delivery/web/api/restricted/localeSelector.go new file mode 100644 index 0000000..8131501 --- /dev/null +++ b/app/delivery/web/api/restricted/localeSelector.go @@ -0,0 +1,52 @@ +package restricted + +import ( + "git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" +) + +// LocaleSelectorHandler for getting languages and countries data +type LocaleSelectorHandler struct { + localeSelectorService *localeSelectorService.LocaleSelectorService +} + +// NewLocaleSelectorHandler creates a new LocaleSelectorHandler instance +func NewLocaleSelectorHandler() *LocaleSelectorHandler { + localeSelectorService := localeSelectorService.New() + return &LocaleSelectorHandler{ + localeSelectorService: localeSelectorService, + } +} + +func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewLocaleSelectorHandler() + + r.Get("/get-languages", handler.GetLanguages) + r.Get("/get-countries", handler.GetCountries) + + return r +} + +func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error { + languages, err := h.localeSelectorService.GetLanguages() + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error { + countries, err := h.localeSelectorService.GetCountriesAndCurrencies() + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/api/restricted/meiliSearch.go b/app/delivery/web/api/restricted/meiliSearch.go index d40a38e..c297d37 100644 --- a/app/delivery/web/api/restricted/meiliSearch.go +++ b/app/delivery/web/api/restricted/meiliSearch.go @@ -25,27 +25,87 @@ func NewMeiliSearchHandler() *MeiliSearchHandler { func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMeiliSearchHandler() + // for superadmin only + r.Get("/create-index", handler.CreateIndex) r.Get("/test", handler.Test) + // for all users + r.Get("/search", handler.Search) + return r } +func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error { + id_lang, 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))) + } + + err := h.meiliService.CreateIndex(id_lang) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + nothing := "" + return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK))) +} + func (h *MeiliSearchHandler) Test(c fiber.Ctx) error { - - id_shop_attribute := c.Query("shopID") - id_shop, err := strconv.Atoi(id_shop_attribute) - if err != nil { + id_lang, 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))) } - 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))) - } - - test, err := h.meiliService.Test(uint(id_shop), uint(id_lang)) + test, err := h.meiliService.Test(id_lang) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *MeiliSearchHandler) Search(c fiber.Ctx) error { + id_lang, 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))) + } + + query := c.Query("query") + + limit_attribute := c.Query("limit") + limit, err := strconv.Atoi(limit_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + id_category_attribute := c.Query("id_category") + id_category, err := strconv.Atoi(id_category_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + price_lower_bound_attribute := c.Query("price_lower_bound") + price_lower_bound, err := strconv.ParseFloat(price_lower_bound_attribute, 64) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + price_upper_bound_attribute := c.Query("price_upper_bound") + price_upper_bound, err := strconv.ParseFloat(price_upper_bound_attribute, 64) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + test, err := h.meiliService.Search(id_lang, query, uint(limit), uint(id_category), price_lower_bound, price_upper_bound) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index 37c2596..74fa700 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -1,8 +1,6 @@ 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" @@ -26,26 +24,33 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMenuHandler() r.Get("/get-menu", handler.GetMenu) + r.Get("/get-routes", handler.GetRouting) return r } func (h *MenuHandler) GetMenu(c fiber.Ctx) error { - - id_shop_attribute := c.Query("shopID") - id_shop, err := strconv.Atoi(id_shop_attribute) - if err != nil { + 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))) } - - 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)) + menu, err := h.menuService.GetMenu(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))) +} + +func (h *MenuHandler) GetRouting(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.GetRoutes(lang_id) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/delivery/web/api/restricted/productDescription.go b/app/delivery/web/api/restricted/productDescription.go index 4cca8a3..a107a21 100644 --- a/app/delivery/web/api/restricted/productDescription.go +++ b/app/delivery/web/api/restricted/productDescription.go @@ -54,13 +54,6 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - productShopID_attribute := c.Query("productShopID") - productShopID, err := strconv.Atoi(productShopID_attribute) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) - } - productLangID_attribute := c.Query("productLangID") productLangID, err := strconv.Atoi(productLangID_attribute) if err != nil { @@ -68,7 +61,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID)) + description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productLangID)) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -77,7 +70,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error { return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK))) } -// SaveProductDescription saves the description for a given product ID, in given shop and language +// SaveProductDescription saves the description for a given product ID, in given language func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { @@ -92,13 +85,6 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - productShopID_attribute := c.Query("productShopID") - productShopID, err := strconv.Atoi(productShopID_attribute) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) - } - productLangID_attribute := c.Query("productLangID") productLangID, err := strconv.Atoi(productLangID_attribute) if err != nil { @@ -112,7 +98,7 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } - err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates) + err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -136,13 +122,6 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - productShopID_attribute := c.Query("productShopID") - productShopID, err := strconv.Atoi(productShopID_attribute) - if err != nil { - return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). - JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) - } - productFromLangID_attribute := c.Query("productFromLangID") productFromLangID, err := strconv.Atoi(productFromLangID_attribute) if err != nil { @@ -163,7 +142,7 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel) + description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 30a372a..c18aaf0 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -97,10 +97,10 @@ func (s *Server) Setup() error { listProducts := s.restricted.Group("/list-products") restricted.ListProductsHandlerRoutes(listProducts) - // changing the JWT cookies routes (restricted) - // in reality it just handles changing user's country and language - langsAndCountries := s.restricted.Group("/langs-and-countries") - restricted.LangsAndCountriesHandlerRoutes(langsAndCountries) + // locale selector (restricted) + // this is basically for changing user's selected language and country + localeSelector := s.restricted.Group("/langs-and-countries") + restricted.LocaleSelectorHandlerRoutes(localeSelector) // menu (restricted) menu := s.restricted.Group("/menu") @@ -110,6 +110,10 @@ func (s *Server) Setup() error { meiliSearch := s.restricted.Group("/meili-search") restricted.MeiliSearchHandlerRoutes(meiliSearch) + s.api.All("*", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusNotFound) + }) + // // Restricted routes example // restricted := s.api.Group("/restricted") // restricted.Use(middleware.AuthMiddleware()) diff --git a/app/model/product.go b/app/model/product.go index eb63f68..3c8faae 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -82,18 +82,27 @@ type ProductFilters struct { } 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"` + 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"` + LinkRewrite string `gorm:"column:link_rewrite"` + IsoCode string `gorm:"column:iso_code"` } 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"` + 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"` + Children []Category `json:"children" form:"children"` +} + +type CategpryParams 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/model/productDescription.go b/app/model/productDescription.go index 2411f59..7bfa078 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -18,3 +18,30 @@ 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"` } + +type ProductRow struct { + IDProduct int `gorm:"column:id_product"` + IDShop int `gorm:"column:id_shop"` + Name string `gorm:"column:name"` + Active uint8 `gorm:"column:active"` + Reference string `gorm:"column:reference"` +} + +type MeiliSearchProduct struct { + ProductID uint `gorm:"column:id_product"` + Name string `gorm:"column:name"` + Active uint8 `gorm:"column:active"` + Price float64 `gorm:"column:price"` + Description string `gorm:"column:description"` + DescriptionShort string `gorm:"column:description_short"` + Usage string `gorm:"column:used_for"` + EAN13 string `gorm:"column:ean13"` + Reference string `gorm:"column:reference"` + Width float64 `gorm:"column:width"` + Height float64 `gorm:"column:height"` + Depth float64 `gorm:"column:depth"` + Weight float64 `gorm:"column:weight"` + CategoryID uint `gorm:"column:id_category"` + CategoryName string `gorm:"column:category_name"` + Variations uint `gorm:"column:variations"` +} diff --git a/app/model/routing.go b/app/model/routing.go new file mode 100644 index 0000000..135f427 --- /dev/null +++ b/app/model/routing.go @@ -0,0 +1,21 @@ +package model + +type Route struct { + ID uint `gorm:"primaryKey;autoIncrement"` + Name string `gorm:"type:varchar(255);not null;unique"` + Path *string `gorm:"type:varchar(255);default:null"` + Component string `gorm:"type:varchar(255);not null;comment:path to component file"` + Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"` + Meta *string `gorm:"type:longtext;default:'{}'"` + IsActive *bool `gorm:"type:tinyint;default:1"` + SortOrder *int `gorm:"type:int;default:0"` + + ParentID *uint `gorm:"index"` + Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"` + + Children []Route `gorm:"foreignKey:ParentID"` +} + +func (Route) TableName() string { + return "b2b_routes" +} diff --git a/app/repos/categoriesRepo/categoriesRepo.go b/app/repos/categoriesRepo/categoriesRepo.go new file mode 100644 index 0000000..d420187 --- /dev/null +++ b/app/repos/categoriesRepo/categoriesRepo.go @@ -0,0 +1,42 @@ +package categoriesRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" +) + +type UICategoriesRepo interface { + GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) +} + +type CategoriesRepo struct{} + +func New() UICategoriesRepo { + return &CategoriesRepo{} +} + +func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) { + var allCategories []model.ScannedCategory + + err := db.DB. + Table("ps_category"). + 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, + ps_category_lang.link_rewrite AS link_rewrite, + ps_lang.iso_code AS iso_code + 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 = ? + JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang + `, + constdata.SHOP_ID, id_lang, constdata.SHOP_ID). + Scan(&allCategories).Error + + return allCategories, err +} diff --git a/app/langs/langs.go b/app/repos/langsRepo/langsRepo.go similarity index 100% rename from app/langs/langs.go rename to app/repos/langsRepo/langsRepo.go diff --git a/repository/listProductsRepo/listProductsRepo.go b/app/repos/listProductsRepo/listProductsRepo.go similarity index 59% rename from repository/listProductsRepo/listProductsRepo.go rename to app/repos/listProductsRepo/listProductsRepo.go index c57cd8f..539b6fc 100644 --- a/repository/listProductsRepo/listProductsRepo.go +++ b/app/repos/listProductsRepo/listProductsRepo.go @@ -3,12 +3,13 @@ package listProductsRepo import ( "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" ) type UIListProductsRepo interface { - GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) + GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) } type ListProductsRepo struct{} @@ -17,7 +18,7 @@ func New() UIListProductsRepo { return &ListProductsRepo{} } -func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { +func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { var listing []model.ProductInList var total int64 @@ -35,41 +36,43 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi // Limit(p.Limit()). // Offset(p.Offset()) - err := db.DB.Raw(` - SELECT - ps_product.id_product AS ID, + subQuery := db.DB. + Table("ps_image"). + Select("id_product, MIN(id_image) AS id_image"). + Group("id_product") + + err := db.DB. + Table("ps_product"). + Select(` + ps_product.id_product AS id, ps_product_lang.name AS name, ps_product.active AS active, ps_product_lang.link_rewrite AS link_rewrite, - COALESCE ( - ps_image_shop.id_image, any_image.id_image - ) AS id_image - FROM ps_product + COALESCE(ps_image_shop.id_image, any_image.id_image) AS id_image + `). + Joins(` LEFT JOIN ps_product_lang - ON ps_product_lang.id_product = ps_product.id_product - AND ps_product_lang.id_shop = ? + ON ps_product_lang.id_product = ps_product.id_product + AND ps_product_lang.id_shop = ? AND ps_product_lang.id_lang = ? + `, constdata.SHOP_ID, id_lang). + Joins(` LEFT JOIN ps_image_shop - ON ps_image_shop.id_product = ps_product.id_product - AND ps_image_shop.id_shop = ? + ON ps_image_shop.id_product = ps_product.id_product + AND ps_image_shop.id_shop = ? AND ps_image_shop.cover = 1 - LEFT JOIN ( - SELECT id_product, MIN(id_image) AS id_image - FROM ps_image - GROUP BY id_product - ) any_image - ON ps_product.id_product = any_image.id_product - LIMIT ? OFFSET ?`, - id_shop, id_lang, id_shop, p.Limit(), p.Offset()). + `, constdata.SHOP_ID). + Joins("LEFT JOIN (?) AS any_image ON ps_product.id_product = any_image.id_product", subQuery). + Limit(p.Limit()). + Offset(p.Offset()). Scan(&listing).Error if err != nil { return find.Found[model.ProductInList]{}, err } - err = db.DB.Raw(` - SELECT COUNT(*) - FROM ps_product`). - Scan(&total).Error + err = db.DB. + Table("ps_product"). + Count(&total).Error if err != nil { return find.Found[model.ProductInList]{}, err } diff --git a/repository/langsAndCountriesRepo/langsAndCountriesRepo.go b/app/repos/localeSelectorRepo/localeSelectorRepo.go similarity index 66% rename from repository/langsAndCountriesRepo/langsAndCountriesRepo.go rename to app/repos/localeSelectorRepo/localeSelectorRepo.go index 22fbacc..f08a57b 100644 --- a/repository/langsAndCountriesRepo/langsAndCountriesRepo.go +++ b/app/repos/localeSelectorRepo/localeSelectorRepo.go @@ -1,22 +1,22 @@ -package langsAndCountriesRepo +package localeSelectorRepo import ( "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" ) -type UILangsAndCountriesRepo interface { +type UILocaleSelectorRepo interface { GetLanguages() ([]model.Language, error) GetCountriesAndCurrencies() ([]model.Country, error) } -type LangsAndCountriesRepo struct{} +type LocaleSelectorRepo struct{} -func New() UILangsAndCountriesRepo { - return &LangsAndCountriesRepo{} +func New() UILocaleSelectorRepo { + return &LocaleSelectorRepo{} } -func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) { +func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) { var languages []model.Language err := db.DB.Table("b2b_language").Scan(&languages).Error @@ -24,7 +24,7 @@ func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) { return languages, err } -func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) { +func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) { var countries []model.Country err := db.DB.Table("b2b_countries"). diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go new file mode 100644 index 0000000..d97020a --- /dev/null +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -0,0 +1,114 @@ +package productDescriptionRepo + +import ( + "fmt" + + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" +) + +type UIProductDescriptionRepo interface { + GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) + CreateIfDoesNotExist(productID uint, productLangID uint) error + UpdateFields(productID uint, productLangID uint, updates map[string]string) error + GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) +} + +type ProductDescriptionRepo struct{} + +func New() UIProductDescriptionRepo { + return &ProductDescriptionRepo{} +} + +// We assume that any user has access to all product descriptions +func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) { + var ProductDescription model.ProductDescription + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + First(&ProductDescription).Error + if err != nil { + return nil, fmt.Errorf("database error: %w", err) + } + + return &ProductDescription, nil +} + +// If it doesn't exist, returns an error. +func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error { + record := model.ProductDescription{ + ProductID: productID, + ShopID: constdata.SHOP_ID, + LangID: productLangID, + } + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + FirstOrCreate(&record).Error + if err != nil { + return fmt.Errorf("database error: %w", err) + } + + return nil +} + +func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error { + if len(updates) == 0 { + return nil + } + updatesIface := make(map[string]interface{}, len(updates)) + for k, v := range updates { + updatesIface[k] = v + } + + err := db.DB. + Table("ps_product_lang"). + Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID). + Updates(updatesIface).Error + if err != nil { + return fmt.Errorf("database error: %w", err) + } + + return nil +} + +// We assume that any user has access to all product descriptions +func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) { + var products []model.MeiliSearchProduct + + err := db.DB. + Select(` + ps.id_product AS id_product, + pl.name AS name, + ps.active AS active, + ps.price AS price, + pl.description AS description, + pl.description_short AS description_short, + pl.usage AS used_for, + p.ean13 AS ean13, + p.reference AS reference, + p.width AS width, + p.height AS height, + p.depth AS depth, + p.weight AS weight, + ps.id_category_default AS id_category, + cl.name AS category_name, + COUNT(DISTINCT pas.id_product_attribute) AS variations + `). + Table("ps_product_shop AS ps"). + Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). + Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). + Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). + Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). + Where("ps.id_shop = ?", constdata.SHOP_ID). + Group("ps.id_product"). + Scan(&products).Error + if err != nil { + return products, fmt.Errorf("database error: %w", err) + } + + return products, nil +} diff --git a/app/repos/routesRepo/routesRepo.go b/app/repos/routesRepo/routesRepo.go new file mode 100644 index 0000000..c70ba86 --- /dev/null +++ b/app/repos/routesRepo/routesRepo.go @@ -0,0 +1,25 @@ +package routesrepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" +) + +type UIRoutesRepo interface { + GetRoutes(langId uint) ([]model.Route, error) +} + +type RoutesRepo struct{} + +func New() UIRoutesRepo { + return &RoutesRepo{} +} + +func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) { + routes := []model.Route{} + err := db.DB.Find(&routes).Error + if err != nil { + return nil, err + } + return routes, nil +} diff --git a/app/service/langsAndCountriesService/langsAndCountriesService.go b/app/service/langsAndCountriesService/langsAndCountriesService.go deleted file mode 100644 index 3c587b5..0000000 --- a/app/service/langsAndCountriesService/langsAndCountriesService.go +++ /dev/null @@ -1,26 +0,0 @@ -package langsAndCountriesService - -import ( - "git.ma-al.com/goc_daniel/b2b/app/model" - "git.ma-al.com/goc_daniel/b2b/repository/langsAndCountriesRepo" -) - -// LangsAndCountriesService literally sends back language and countries information. -type LangsAndCountriesService struct { - repo langsAndCountriesRepo.UILangsAndCountriesRepo -} - -// NewLangsAndCountriesService creates a new LangsAndCountries service -func New() *LangsAndCountriesService { - return &LangsAndCountriesService{ - repo: langsAndCountriesRepo.New(), - } -} - -func (s *LangsAndCountriesService) GetLanguages() ([]model.Language, error) { - return s.repo.GetLanguages() -} - -func (s *LangsAndCountriesService) GetCountriesAndCurrencies() ([]model.Country, error) { - return s.repo.GetCountriesAndCurrencies() -} diff --git a/app/service/langsService/service.go b/app/service/langsService/service.go index 14efec7..fac8f06 100644 --- a/app/service/langsService/service.go +++ b/app/service/langsService/service.go @@ -1,7 +1,7 @@ package langsService import ( - langs_repo "git.ma-al.com/goc_daniel/b2b/app/langs" + langs_repo "git.ma-al.com/goc_daniel/b2b/app/repos/langsRepo" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/response" diff --git a/app/service/listProductsService/listProductsService.go b/app/service/listProductsService/listProductsService.go index 8392731..93b5ddc 100644 --- a/app/service/listProductsService/listProductsService.go +++ b/app/service/listProductsService/listProductsService.go @@ -2,9 +2,9 @@ package listProductsService import ( "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/listProductsRepo" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" - "git.ma-al.com/goc_daniel/b2b/repository/listProductsRepo" ) type ListProductsService struct { @@ -17,7 +17,7 @@ func New() *ListProductsService { } } -func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { +func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { var products find.Found[model.ProductInList] // currencyIso := c.Cookies("currency_iso", "") @@ -30,7 +30,7 @@ func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Pagi // countryIso = overrides["override_country"] // } - products, err := s.listProductsRepo.GetListing(id_shop, id_lang, p, filters) + products, err := s.listProductsRepo.GetListing(id_lang, p, filters) if err != nil { return products, err } diff --git a/app/service/localeSelectorService/localeSelectorService.go b/app/service/localeSelectorService/localeSelectorService.go new file mode 100644 index 0000000..7f781dc --- /dev/null +++ b/app/service/localeSelectorService/localeSelectorService.go @@ -0,0 +1,26 @@ +package localeSelectorService + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/localeSelectorRepo" +) + +// LocaleSelectorService literally sends back language and countries information. +type LocaleSelectorService struct { + repo localeSelectorRepo.UILocaleSelectorRepo +} + +// NewLocaleSelectorService creates a new LocaleSelector service +func New() *LocaleSelectorService { + return &LocaleSelectorService{ + repo: localeSelectorRepo.New(), + } +} + +func (s *LocaleSelectorService) GetLanguages() ([]model.Language, error) { + return s.repo.GetLanguages() +} + +func (s *LocaleSelectorService) GetCountriesAndCurrencies() ([]model.Country, error) { + return s.repo.GetCountriesAndCurrencies() +} diff --git a/app/service/meiliService/blank.json b/app/service/meiliService/blank.json new file mode 100644 index 0000000..22446a1 --- /dev/null +++ b/app/service/meiliService/blank.json @@ -0,0 +1,8 @@ +{ + "products-openai": { + "source": "openAi", + "model": "text-embedding-3-small", + "apiKey": "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", + "documentTemplate": "{{doc.Name}} is equipment used for {{doc.Description | truncatewords: 20}}" + } +} \ No newline at end of file diff --git a/app/service/meiliService/meiliService.go b/app/service/meiliService/meiliService.go index ab717ff..c58bfa1 100644 --- a/app/service/meiliService/meiliService.go +++ b/app/service/meiliService/meiliService.go @@ -1,15 +1,22 @@ package meiliService import ( + "encoding/xml" "fmt" + "io" "os" "strconv" + "strings" + "time" + "git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "github.com/meilisearch/meilisearch-go" ) type MeiliService struct { - meiliClient meilisearch.ServiceManager + productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo + meiliClient meilisearch.ServiceManager } func New() *MeiliService { @@ -22,34 +29,157 @@ func New() *MeiliService { ) return &MeiliService{ - meiliClient: client, + meiliClient: client, + productDescriptionRepo: productDescriptionRepo.New(), } } +// ==================================== FOR SUPERADMIN ONLY ==================================== +func (s *MeiliService) CreateIndex(id_lang uint) error { + indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + + products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang) + for i := 0; i < len(products); i++ { + products[i].Description, err = cleanHTML(products[i].Description) + if err != nil { + fmt.Printf("products[i].Description: %v\n", products[i].Description) + fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID) + fmt.Println("failed at description") + fmt.Printf("err: %v\n", err) + return err + } + + products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort) + if err != nil { + fmt.Printf("products[i].DescriptionShort: %v\n", products[i].DescriptionShort) + fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID) + fmt.Println("failed at description short") + fmt.Printf("err: %v\n", err) + return err + } + + products[i].Usage, err = cleanHTML(products[i].Usage) + if err != nil { + fmt.Printf("products[i].Usage: %v\n", products[i].Usage) + fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID) + fmt.Println("failed at usage") + fmt.Printf("err: %v\n", err) + return err + } + } + + primaryKey := "ProductID" + docOptions := &meilisearch.DocumentOptions{ + PrimaryKey: &primaryKey, + SkipCreation: false, + } + task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + filterableAttributes := []interface{}{ + "CategoryID", + "Price", + } + task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + displayedAttributes := []string{ + "ProductID", + "Name", + "Active", + "Price", + "EAN13", + "Reference", + "Variations", + } + task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + searchableAttributes := []string{ + "Name", + "DescriptionShort", + "Reference", + "EAN13", + "CategoryName", + "Description", + "Usage", + } + task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + return err +} + // ==================================== FOR DEBUG ONLY ==================================== -func (s *MeiliService) Test(id_shop uint, id_lang uint) (meilisearch.SearchResponse, error) { - indexName := "products_lang" + strconv.FormatInt(int64(id_lang), 10) +func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) { + indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) searchReq := &meilisearch.SearchRequest{ - Limit: 3, + Limit: 4, + Facets: []string{ + "CategoryID", + }, } // Perform search - results, err := s.meiliClient.Index(indexName).Search("walek", searchReq) + results, err := s.meiliClient.Index(indexName).Search("mat", searchReq) if err != nil { fmt.Printf("Meilisearch error: %v\n", err) return meilisearch.SearchResponse{}, err } - fmt.Printf("Search results for query 'walek' in %s: %d hits\n", indexName, len(results.Hits)) + fmt.Printf("Search results for query 'mat' in %s: %d hits\n", indexName, len(results.Hits)) return *results, nil } // Search performs a full-text search on the specified index -func (s *MeiliService) Search(indexName string, query string, limit int) (meilisearch.SearchResponse, error) { +func (s *MeiliService) Search(id_lang uint, query string, limit uint, id_category uint, price_lower_bound float64, price_upper_bound float64) (meilisearch.SearchResponse, error) { + indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + + filter_query := "" + if id_category != 0 { + filter_query = "CategoryID = " + strconv.FormatUint(uint64(id_category), 10) + } + if price_lower_bound > 0.0 { + if filter_query != "" { + filter_query += " AND " + } + filter_query += "Price >= " + strconv.FormatFloat(price_lower_bound, 'f', -1, 64) + } + if price_upper_bound > 0.0 { + if filter_query != "" { + filter_query += " AND " + } + filter_query += "Price <= " + strconv.FormatFloat(price_upper_bound, 'f', -1, 64) + } + searchReq := &meilisearch.SearchRequest{ Limit: int64(limit), + Facets: []string{ + "CategoryID", + }, + Filter: filter_query, } results, err := s.meiliClient.Index(indexName).Search(query, searchReq) @@ -70,3 +200,41 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) { return health, nil } + +// remove all tags from HTML text +func cleanHTML(s string) (string, error) { + r := strings.NewReader(s) + d := xml.NewDecoder(r) + + text := "" + + // Configure the decoder for HTML; leave off strict and autoclose for XHTML + d.Strict = true + d.AutoClose = xml.HTMLAutoClose + d.Entity = xml.HTMLEntity + for { + token, err := d.Token() + if err == io.EOF { + break + } else if err != nil { + return text, err + } + + switch v := token.(type) { + case xml.StartElement: + if len(text) > 0 && text[len(text)-1] != '\n' { + text += " \n " + } + case xml.EndElement: + case xml.CharData: + if strings.TrimSpace(string(v)) != "" { + text += string(v) + } + case xml.Comment: + case xml.ProcInst: + case xml.Directive: + } + } + + return text, nil +} diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index 3186233..f4b9994 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -4,24 +4,27 @@ import ( "sort" "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo" + routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo" "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 + routesRepo routesRepo.UIRoutesRepo } func New() *MenuService { return &MenuService{ categoriesRepo: categoriesRepo.New(), + routesRepo: routesRepo.New(), } } -func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error) { - all_categories, err := s.categoriesRepo.GetAllCategories(id_shop, id_lang) +func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) { + all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) if err != nil { - return model.Category{}, err + return &model.Category{}, err } // find the root @@ -35,7 +38,7 @@ func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error } } if !root_found { - return model.Category{}, responseErrors.ErrNoRootFound + return &model.Category{}, responseErrors.ErrNoRootFound } // now create the children and reorder them according to position @@ -57,25 +60,31 @@ func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error // finally, create the tree tree := s.createTree(root_index, &all_categories, &children_indices) - return tree, nil + 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)) + node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)) } return node } +func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) { + return s.routesRepo.GetRoutes(id_lang) +} + func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category { var normal model.Category - normal.Active = scanned.Active + // normal.Active = scanned.Active normal.CategoryID = scanned.CategoryID - normal.Name = scanned.Name - normal.Subcategories = []model.Category{} + normal.Label = scanned.Name + // normal.Active = scanned.Active == 1 + normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} + normal.Children = []model.Category{} return normal } diff --git a/app/service/productDescriptionService/productDescriptionService.go b/app/service/productDescriptionService/productDescriptionService.go index 4f4f136..940ea41 100644 --- a/app/service/productDescriptionService/productDescriptionService.go +++ b/app/service/productDescriptionService/productDescriptionService.go @@ -17,9 +17,9 @@ import ( "cloud.google.com/go/translate/apiv3/translatepb" "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo" "git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" - "git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/responses" @@ -70,7 +70,7 @@ func New() *ProductDescriptionService { log.Fatalf("productDescriptionService: cannot create Translation client: %v", err) } - openAIClient := openai.NewClient(option.WithAPIKey("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"), + openAIClient := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_KEY")), option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout return &ProductDescriptionService{ @@ -82,12 +82,12 @@ func New() *ProductDescriptionService { } } -func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { - return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID) +func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) { + return s.productDescriptionRepo.GetProductDescription(productID, productLangID) } // Updates relevant fields with the "updates" map -func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error { +func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error { // only some fields can be affected allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"} for key := range updates { @@ -106,12 +106,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI } } - err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID) + err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productLangID) if err != nil { return err } - return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates) + return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates) } // TranslateProductDescription fetches the product description for productFromLangID, @@ -120,9 +120,9 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI // // The Google Cloud project must have the Cloud Translation API enabled and the // service account must hold the "Cloud Translation API User" role. -func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) { +func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) { - productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID) + productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productFromLangID) if err != nil { return nil, err } diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index a411036..8bdce27 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -2,3 +2,4 @@ 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 diff --git a/bo/components.d.ts b/bo/components.d.ts index 622aa17..85bba39 100644 --- a/bo/components.d.ts +++ b/bo/components.d.ts @@ -11,14 +11,21 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + Cart1: typeof import('./src/components/customer/Cart1.vue')['default'] + CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default'] + CompanyAccountView: typeof import('./src/components/customer/CompanyAccountView.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'] + PageAccount: typeof import('./src/components/customer/PageAccount.vue')['default'] PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] PageCart: typeof import('./src/components/customer/PageCart.vue')['default'] + PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default'] + PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default'] PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default'] + PageProductsList: typeof import('./src/components/customer/PageProductsList.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'] @@ -41,6 +48,7 @@ declare module 'vue' { 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'] diff --git a/bo/src/components/TopBar.vue b/bo/src/components/TopBar.vue index 6f0b00d..f999875 100644 --- a/bo/src/components/TopBar.vue +++ b/bo/src/components/TopBar.vue @@ -28,9 +28,18 @@ const authStore = useAuthStore() Addresses + + Customer Data + Cart + + Cart1 + + + Products List +
diff --git a/bo/src/components/TopBarLogin.vue b/bo/src/components/TopBarLogin.vue index 129a58e..7382dc8 100644 --- a/bo/src/components/TopBarLogin.vue +++ b/bo/src/components/TopBarLogin.vue @@ -14,9 +14,9 @@ const authStore = useAuthStore()
- +
- TimeTracker + B2B
diff --git a/bo/src/components/admin/ProductDetailView.vue b/bo/src/components/admin/ProductDetailView.vue index e5a8259..2ce5806 100644 --- a/bo/src/components/admin/ProductDetailView.vue +++ b/bo/src/components/admin/ProductDetailView.vue @@ -1,4 +1,6 @@ diff --git a/bo/src/components/customer/Cart1.vue b/bo/src/components/customer/Cart1.vue new file mode 100644 index 0000000..2ae6fbd --- /dev/null +++ b/bo/src/components/customer/Cart1.vue @@ -0,0 +1,74 @@ + + + diff --git a/bo/src/components/customer/PageAddresses.vue b/bo/src/components/customer/PageAddresses.vue index 2152eee..c4f9410 100644 --- a/bo/src/components/customer/PageAddresses.vue +++ b/bo/src/components/customer/PageAddresses.vue @@ -1,8 +1,9 @@ \ No newline at end of file diff --git a/bo/src/components/customer/PageCustomerData.vue b/bo/src/components/customer/PageCustomerData.vue new file mode 100644 index 0000000..dbe2448 --- /dev/null +++ b/bo/src/components/customer/PageCustomerData.vue @@ -0,0 +1,110 @@ + + + \ No newline at end of file diff --git a/bo/src/components/customer/PageProductCardFull.vue b/bo/src/components/customer/PageProductCardFull.vue index b68ba07..fdff612 100644 --- a/bo/src/components/customer/PageProductCardFull.vue +++ b/bo/src/components/customer/PageProductCardFull.vue @@ -1,8 +1,9 @@ \ No newline at end of file diff --git a/bo/src/components/customer/components/ProductCustomization.vue b/bo/src/components/customer/components/ProductCustomization.vue index 4470502..d67410b 100644 --- a/bo/src/components/customer/components/ProductCustomization.vue +++ b/bo/src/components/customer/components/ProductCustomization.vue @@ -4,7 +4,7 @@

Product customization

Don't forget to save your customization to be able to add to cart

-
+
diff --git a/bo/src/components/inner/categoryMenu.vue b/bo/src/components/inner/categoryMenu.vue new file mode 100644 index 0000000..a370c3c --- /dev/null +++ b/bo/src/components/inner/categoryMenu.vue @@ -0,0 +1,40 @@ + + + diff --git a/bo/src/layouts/default.vue b/bo/src/layouts/default.vue index 1e86a0a..c0618e5 100644 --- a/bo/src/layouts/default.vue +++ b/bo/src/layouts/default.vue @@ -1,14 +1,12 @@ diff --git a/bo/src/layouts/empty.vue b/bo/src/layouts/empty.vue index e8cfec0..e665e66 100644 --- a/bo/src/layouts/empty.vue +++ b/bo/src/layouts/empty.vue @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/bo/src/views/LoginView.vue b/bo/src/views/LoginView.vue index 20cec7b..eeceb93 100644 --- a/bo/src/views/LoginView.vue +++ b/bo/src/views/LoginView.vue @@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { useValidation } from '@/composable/useValidation' import type { FormError } from '@nuxt/ui' -import { useI18n } from 'vue-i18n' import { i18n } from '@/plugins/02_i18n' +import Empty from '@/layouts/empty.vue' const router = useRouter() const route = useRoute() @@ -52,9 +52,11 @@ const PrivacyComponent = computed(() => import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')), ), ) +