Compare commits
32 Commits
routing
...
f6b321b602
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b321b602 | ||
|
|
af91842b14 | ||
| 04e238fd66 | |||
|
|
e0c53c97ba | ||
| 09a77c14c9 | |||
|
|
c7533a8deb | ||
|
|
1bab7f642f | ||
|
|
a988bbbc33 | ||
|
|
395d670298 | ||
|
|
7d4242abb1 | ||
|
|
9c7eb5ee4e | ||
|
|
833f4a5a07 | ||
|
|
b9bc121d43 | ||
|
|
b2acb8c922 | ||
| cf4d14a3cb | |||
| 30eb82ba53 | |||
| a2a2c35ab3 | |||
|
|
03f04b2f53 | ||
|
|
55da953f32 | ||
| 684f910090 | |||
| 5feaa9e15c | |||
|
|
04e2549a66 | ||
| fb4f7048ab | |||
|
|
a3f01eca7c | ||
|
|
1fa6206b75 | ||
|
|
fa89723eb6 | ||
|
|
8665c566ee | ||
| ec5ff123ac | |||
| 17317e778c | |||
| 94291ccc03 | |||
| 91c5de1f67 | |||
|
|
d0ce65c287 |
6
.env
6
.env
@@ -48,6 +48,10 @@ EMAIL_FROM=test@ma-al.com
|
||||
EMAIL_FROM_NAME=Gitea Manager
|
||||
EMAIL_ADMIN=goc_marek@ma-al.pl
|
||||
|
||||
# STORAGE
|
||||
STORAGE_ROOT=./storage
|
||||
|
||||
|
||||
I18N_LANGS=en,pl,cs
|
||||
|
||||
PDF_SERVER_URL=http://localhost:8000
|
||||
@@ -58,3 +62,5 @@ FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
||||
|
||||
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
|
||||
CORS_ORGIN=https://www.naluconcept.com
|
||||
|
||||
DSN=root:Maal12345678@tcp(localhost:3306)/nalu
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ bin/
|
||||
i18n/*.json
|
||||
*_templ.go
|
||||
tmp/main
|
||||
test.go
|
||||
test.go
|
||||
storage/*
|
||||
!storage/.gitkeep
|
||||
@@ -1127,21 +1127,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/menu/get-menu": {
|
||||
"/api/v1/restricted/menu/get-category-tree": {
|
||||
"get": {
|
||||
"tags": ["Menu"],
|
||||
"summary": "Get menu structure",
|
||||
"description": "Returns the menu structure for the current language. Requires authentication.",
|
||||
"operationId": "getMenu",
|
||||
"summary": "Get category tree",
|
||||
"description": "Returns the category tree rooted at the given category ID for the current language. Requires authentication.",
|
||||
"operationId": "getCategoryTree",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": [],
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "root_category_id",
|
||||
"in": "query",
|
||||
"description": "Root category ID to build the tree from",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Menu retrieved successfully",
|
||||
"description": "Category tree retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -1151,7 +1162,73 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Invalid request or root category not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/restricted/menu/get-breadcrumb": {
|
||||
"get": {
|
||||
"tags": ["Menu"],
|
||||
"summary": "Get breadcrumb",
|
||||
"description": "Returns the breadcrumb path from the root category to the specified category for the current language. Requires authentication.",
|
||||
"operationId": "getBreadcrumb",
|
||||
"security": [
|
||||
{
|
||||
"CookieAuth": [],
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "root_category_id",
|
||||
"in": "query",
|
||||
"description": "Root category ID (breadcrumb starting point)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"in": "query",
|
||||
"description": "Target category ID (breadcrumb destination)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Breadcrumb retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request, category not found, or root never reached",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -1221,7 +1298,23 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/B2BTopMenu"
|
||||
},
|
||||
"description": "Root menu items with nested children"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Number of root menu items"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1995,46 +2088,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MenuItem": {
|
||||
"type": "object",
|
||||
"description": "Menu item structure",
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"description": "Category ID"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Menu item label"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/components/schemas/MenuItemParams"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MenuItem"
|
||||
},
|
||||
"description": "Child menu items"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MenuItemParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
},
|
||||
"link_rewrite": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Route": {
|
||||
"type": "object",
|
||||
"description": "Application route",
|
||||
@@ -2338,6 +2391,58 @@
|
||||
"description": "Build date in RFC3339 format"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CategoryInBreadcrumb": {
|
||||
"type": "object",
|
||||
"description": "A single item in a category breadcrumb path",
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"description": "Category ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Category name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"B2BTopMenu": {
|
||||
"type": "object",
|
||||
"description": "Top-level menu item for B2B back-office",
|
||||
"properties": {
|
||||
"menu_id": {
|
||||
"type": "integer",
|
||||
"description": "Menu item ID"
|
||||
},
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "Menu label as JSON (multilingual, e.g. {\"en\": \"Dashboard\", \"pl\": \"Panel\"})"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "integer",
|
||||
"description": "Parent menu ID (null for root items)"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Menu item parameters as JSON"
|
||||
},
|
||||
"active": {
|
||||
"type": "integer",
|
||||
"description": "Active status (1 = active, 0 = inactive)"
|
||||
},
|
||||
"position": {
|
||||
"type": "integer",
|
||||
"description": "Sort position"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/B2BTopMenu"
|
||||
},
|
||||
"description": "Child menu items"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
|
||||
@@ -2,8 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,7 +26,8 @@ type Config struct {
|
||||
GoogleTranslate GoogleTranslateConfig
|
||||
Image ImageConfig
|
||||
Cors CorsConfig
|
||||
MailiSearch MeiliSearchConfig
|
||||
MeiliSearch MeiliSearchConfig
|
||||
Storage StorageConfig
|
||||
}
|
||||
|
||||
type I18n struct {
|
||||
@@ -95,6 +98,10 @@ type EmailConfig struct {
|
||||
Enabled bool `env:"EMAIL_ENABLED,false"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
RootFolder string `env:"STORAGE_ROOT"`
|
||||
}
|
||||
|
||||
type PdfPrinter struct {
|
||||
ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"`
|
||||
}
|
||||
@@ -155,7 +162,7 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.OAuth.Google)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for outh google : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for oauth google : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.App)
|
||||
@@ -170,12 +177,12 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.I18n)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for email : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for i18n : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.Pdf)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for email : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for pdf : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.GoogleTranslate)
|
||||
@@ -185,19 +192,25 @@ func load() *Config {
|
||||
|
||||
err = loadEnv(&cfg.Image)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for image : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.Cors)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for cors : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.MailiSearch)
|
||||
err = loadEnv(&cfg.MeiliSearch)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||
slog.Error("not possible to load env variables for meili search : ", err.Error(), "")
|
||||
}
|
||||
|
||||
err = loadEnv(&cfg.Storage)
|
||||
if err != nil {
|
||||
slog.Error("not possible to load env variables for storage : ", err.Error(), "")
|
||||
}
|
||||
cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -308,6 +321,22 @@ func setValue(field reflect.Value, val string, key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResolveRelativePath(relativePath string) string {
|
||||
// get working directory (where program was started)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// convert to absolute path
|
||||
absPath := relativePath
|
||||
if !filepath.IsAbs(absPath) {
|
||||
absPath = filepath.Join(wd, absPath)
|
||||
}
|
||||
|
||||
return filepath.Clean(absPath)
|
||||
}
|
||||
|
||||
func parseEnvTag(tag string) (key string, def *string) {
|
||||
if tag == "" {
|
||||
return "", nil
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/service/authService"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
@@ -60,9 +62,52 @@ func AuthMiddleware() fiber.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// Set user in context
|
||||
c.Locals(constdata.USER_LOCALES_NAME, user.ToSession())
|
||||
c.Locals(constdata.USER_LOCALES_ID, user.ID)
|
||||
// Create locale. LangID is overwritten by auth Token
|
||||
var userLocale model.UserLocale
|
||||
userLocale.OriginalUser = user
|
||||
|
||||
// Check if target user is present
|
||||
targetUserIDAttribute := c.Query("target_user_id")
|
||||
|
||||
if targetUserIDAttribute == "" {
|
||||
userLocale.User = user
|
||||
c.Locals(constdata.USER_LOCALE, &userLocale)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// We now populate the target user
|
||||
if user.Role != model.RoleAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "admin access required",
|
||||
})
|
||||
}
|
||||
|
||||
targetUserID, err := strconv.Atoi(targetUserIDAttribute)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "invalid target user id attribute",
|
||||
})
|
||||
}
|
||||
|
||||
// to verify target user, we use the same functionality as for verifying original user
|
||||
// Get target user from database
|
||||
user, err = authService.GetUserByID(uint(targetUserID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "target user not found",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if target user is active
|
||||
if !user.IsActive {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "target user account is inactive",
|
||||
})
|
||||
}
|
||||
|
||||
userLocale.User = user
|
||||
c.Locals(constdata.USER_LOCALE, &userLocale)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
@@ -71,21 +116,14 @@ func AuthMiddleware() fiber.Handler {
|
||||
// RequireAdmin creates admin-only middleware
|
||||
func RequireAdmin() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "not authenticated",
|
||||
})
|
||||
}
|
||||
|
||||
userSession, ok := user.(*model.UserSession)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "invalid user session",
|
||||
})
|
||||
}
|
||||
|
||||
if userSession.Role != model.RoleAdmin {
|
||||
if originalUserRole != model.RoleAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "admin access required",
|
||||
})
|
||||
@@ -95,24 +133,6 @@ func RequireAdmin() fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserID extracts user ID from context
|
||||
func GetUserID(c fiber.Ctx) uint {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return userID
|
||||
}
|
||||
|
||||
// GetUser extracts user from context
|
||||
func GetUser(c fiber.Ctx) *model.UserSession {
|
||||
user, ok := c.Locals("user").(*model.UserSession)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// GetConfig returns the app config
|
||||
func GetConfig() *config.Config {
|
||||
return config.Get()
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
@@ -22,12 +24,8 @@ func LanguageMiddleware() fiber.Handler {
|
||||
if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,12 +36,8 @@ func LanguageMiddleware() fiber.Handler {
|
||||
if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,8 +51,7 @@ func LanguageMiddleware() fiber.Handler {
|
||||
lang, err := langService.GetLanguageByISOCode(isoCode)
|
||||
if err == nil && lang != nil {
|
||||
langID = uint(lang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -68,8 +61,7 @@ func LanguageMiddleware() fiber.Handler {
|
||||
defaultLang, err := langService.GetDefaultLanguage()
|
||||
if err == nil && defaultLang != nil {
|
||||
langID = uint(defaultLang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", defaultLang)
|
||||
c.Locals(constdata.USER_LOCALE, returnNewLocale(langID))
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
@@ -104,11 +96,9 @@ func parseAcceptLanguage(header string) string {
|
||||
return strings.ToLower(first)
|
||||
}
|
||||
|
||||
// GetLanguageID extracts language ID from context
|
||||
func GetLanguageID(c fiber.Ctx) uint {
|
||||
langID, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return langID
|
||||
func returnNewLocale(lang_id uint) *model.UserLocale {
|
||||
newLocale := model.UserLocale{}
|
||||
newLocale.OriginalUser = &model.Customer{}
|
||||
newLocale.OriginalUser.LangID = lang_id
|
||||
return &newLocale
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ package public
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/authService"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"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"
|
||||
@@ -264,15 +268,15 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
|
||||
|
||||
// Me returns the current user info
|
||||
func (h *AuthHandler) Me(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
userLocale := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if userLocale.OriginalUser == nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"user": user,
|
||||
"user": *userLocale.OriginalUser,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -345,9 +349,49 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusCreated).JSON(response)
|
||||
}
|
||||
|
||||
// Updates JWT Tokens
|
||||
// Updates JWT Tokens. Requires authentication and updates access token only
|
||||
func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
|
||||
return h.authService.UpdateJWTToken(c)
|
||||
userLocale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
|
||||
}
|
||||
|
||||
// Parse language and country_id from query params
|
||||
langIDStr := c.Query("lang_id")
|
||||
|
||||
if langIDStr != "" {
|
||||
parsedID, err := strconv.ParseUint(langIDStr, 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
|
||||
}
|
||||
userLocale.OriginalUser.LangID = uint(parsedID)
|
||||
}
|
||||
|
||||
countryIDStr := c.Query("country_id")
|
||||
|
||||
if countryIDStr != "" {
|
||||
parsedID, err := strconv.ParseUint(countryIDStr, 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
|
||||
}
|
||||
userLocale.OriginalUser.CountryID = uint(parsedID)
|
||||
}
|
||||
|
||||
newAccessToken, err := h.authService.UpdateJWTToken(userLocale.OriginalUser)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
// does not reset refresh token
|
||||
h.setAuthCookies(c, newAccessToken, "")
|
||||
|
||||
return c.JSON(response.Make(&fiber.Map{"token": newAccessToken}, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
// GoogleLogin redirects the user to Google's OAuth2 consent page
|
||||
@@ -414,12 +458,12 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
||||
|
||||
// Redirect to the locale-prefixed charts page after successful Google login.
|
||||
// The user's preferred language is stored in the auth response; fall back to "en".
|
||||
lang, err := h.authService.GetLangISOCode(response.User.LangID)
|
||||
lang_iso_code, err := h.authService.GetLangISOCode(response.User.LangID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadLangID)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang)
|
||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang_iso_code)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package public
|
||||
import (
|
||||
"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/localeExtractor"
|
||||
"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"
|
||||
@@ -30,7 +31,7 @@ func RoutingHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *RoutingHandler) GetRouting(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
lang_id, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/cartsService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"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"
|
||||
@@ -37,7 +38,7 @@ func CartsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -53,7 +54,7 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -78,7 +79,7 @@ func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -94,7 +95,7 @@ func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -117,7 +118,7 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
|
||||
99
app/delivery/web/api/restricted/list.go
Normal file
99
app/delivery/web/api/restricted/list.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"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/service/listService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ListHandler handles endpoints that list various things (e.g. products or users)
|
||||
type ListHandler struct {
|
||||
listService *listService.ListService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewListHandler creates a new ListHandler instance
|
||||
func NewListHandler() *ListHandler {
|
||||
listService := listService.New()
|
||||
return &ListHandler{
|
||||
listService: listService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func ListHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewListHandler()
|
||||
|
||||
r.Get("/list-products", handler.ListProducts)
|
||||
r.Get("/list-users", handler.ListUsers)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *ListHandler) ListProducts(c fiber.Ctx) error {
|
||||
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
id_lang, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
list, err := h.listService.ListProducts(id_lang, paging, filters)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
var columnMappingListProducts map[string]string = map[string]string{
|
||||
"product_id": "ps.id_product",
|
||||
"name": "pl.name",
|
||||
"reference": "p.reference",
|
||||
"category_name": "cl.name",
|
||||
"category_id": "cp.id_category",
|
||||
"quantity": "sa.quantity",
|
||||
}
|
||||
|
||||
func (h *ListHandler) ListUsers(c fiber.Ctx) error {
|
||||
paging, filters, err := query_params.ParseFilters[model.Customer](c, columnMappingListUsers)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
id_lang, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
list, err := h.listService.ListUsers(id_lang, paging, filters)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
var columnMappingListUsers map[string]string = map[string]string{
|
||||
"user_id": "users.id",
|
||||
"email": "users.email",
|
||||
"first_name": "users.first_name",
|
||||
"second_name": "users.second_name",
|
||||
"role": "users.role",
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"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/service/listProductsService"
|
||||
"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/query/query_params"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
|
||||
type ListProductsHandler struct {
|
||||
listProductsService *listProductsService.ListProductsService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewListProductsHandler creates a new ListProductsHandler instance
|
||||
func NewListProductsHandler() *ListProductsHandler {
|
||||
listProductsService := listProductsService.New()
|
||||
return &ListProductsHandler{
|
||||
listProductsService: listProductsService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewListProductsHandler()
|
||||
|
||||
r.Get("/get-listing", handler.GetListing)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingGetListing)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
var columnMappingGetListing map[string]string = map[string]string{
|
||||
"product_id": "ps.id_product",
|
||||
"name": "pl.name",
|
||||
"reference": "p.reference",
|
||||
"category_name": "cl.name",
|
||||
"category_id": "cp.id_category",
|
||||
"quantity": "sa.quantity",
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
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/localeExtractor"
|
||||
"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"
|
||||
@@ -23,29 +26,68 @@ func NewMenuHandler() *MenuHandler {
|
||||
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewMenuHandler()
|
||||
|
||||
r.Get("/get-menu", handler.GetMenu)
|
||||
r.Get("/get-category-tree", handler.GetCategoryTree)
|
||||
r.Get("/get-breadcrumb", handler.GetBreadcrumb)
|
||||
r.Get("/get-top-menu", handler.GetTopMenu)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
|
||||
lang_id, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
menu, err := h.menuService.GetMenu(lang_id)
|
||||
|
||||
root_category_id_attribute := c.Query("root_category_id")
|
||||
root_category_id, err := strconv.Atoi(root_category_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||
return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
|
||||
lang_id, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
root_category_id_attribute := c.Query("root_category_id")
|
||||
root_category_id, err := strconv.Atoi(root_category_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
category_id_attribute := c.Query("category_id")
|
||||
category_id, err := strconv.Atoi(category_id_attribute)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
|
||||
breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&breadcrumb, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
lang_id, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"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"
|
||||
@@ -41,7 +42,7 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
|
||||
// GetProductDescription returns the product description for a given product ID
|
||||
func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -72,7 +73,7 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
|
||||
|
||||
// SaveProductDescription saves the description for a given product ID, in given language
|
||||
func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
@@ -109,7 +110,7 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||
|
||||
// TranslateProductDescription returns translated product description
|
||||
func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
userID, ok := localeExtractor.GetUserID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
|
||||
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"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"
|
||||
@@ -36,7 +37,7 @@ func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||
id_lang, ok := c.Locals("langID").(uint)
|
||||
id_lang, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
@@ -49,12 +50,11 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
nothing := ""
|
||||
return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK)))
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
|
||||
id_lang, ok := c.Locals("langID").(uint)
|
||||
id_lang, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
@@ -88,7 +88,7 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error {
|
||||
id_lang, ok := c.Locals("langID").(uint)
|
||||
id_lang, ok := localeExtractor.GetLangID(c)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
|
||||
193
app/delivery/web/api/restricted/storage.go
Normal file
193
app/delivery/web/api/restricted/storage.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
|
||||
"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"
|
||||
)
|
||||
|
||||
type StorageHandler struct {
|
||||
storageService *storageService.StorageService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewStorageHandler() *StorageHandler {
|
||||
return &StorageHandler{
|
||||
storageService: storageService.New(),
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func StorageHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewStorageHandler()
|
||||
|
||||
r.Get("/list-content/*", handler.ListContent)
|
||||
r.Get("/download-file/*", handler.DownloadFile)
|
||||
|
||||
r.Get("/move/*", handler.Move)
|
||||
r.Get("/copy/*", handler.Copy)
|
||||
r.Post("/upload-file/*", handler.UploadFile)
|
||||
r.Get("/create-folder/*", handler.CreateFolder)
|
||||
r.Delete("/delete-file/*", handler.DeleteFile)
|
||||
r.Delete("/delete-folder/*", handler.DeleteFolder)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *StorageHandler) Move(c fiber.Ctx) error {
|
||||
src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
err = h.storageService.Move(src_abs_path, dest_abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) Copy(c fiber.Ctx) error {
|
||||
src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
err = h.storageService.Copy(src_abs_path, dest_abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
// accepted path looks like e.g. "/folder1/" or "folder1"
|
||||
func (h *StorageHandler) ListContent(c fiber.Ctx) error {
|
||||
// relative path defaults to root directory
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
entries_in_list, err := h.storageService.ListContent(abs_path)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(entries_in_list, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
c.Attachment(filename)
|
||||
c.Set("Content-Length", strconv.FormatInt(filesize, 10))
|
||||
c.Set("Content-Type", "application/octet-stream")
|
||||
return c.SendStream(f, int(filesize))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) UploadFile(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
f, err := c.FormFile("document")
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrMissingFileFieldDocument)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument)))
|
||||
}
|
||||
|
||||
err = h.storageService.UploadFile(c, abs_path, f)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) CreateFolder(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
err = h.storageService.CreateFolder(abs_path, c.Query("name"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) DeleteFile(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
err = h.storageService.DeleteFile(abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error {
|
||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
err = h.storageService.DeleteFolder(abs_path)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -21,10 +21,12 @@ type SettingsResponse struct {
|
||||
|
||||
// AppSettings represents app configuration
|
||||
type AppSettings struct {
|
||||
Name string `json:"name"`
|
||||
Environment string `json:"environment"`
|
||||
BaseURL string `json:"base_url"`
|
||||
PasswordRegex string `json:"password_regex"`
|
||||
Name string `json:"name"`
|
||||
Environment string `json:"environment"`
|
||||
BaseURL string `json:"base_url"`
|
||||
PasswordRegex string `json:"password_regex"`
|
||||
CategoryTreeRootID uint `json:"category_tree_root_id"`
|
||||
ShopDefaultLanguage uint `json:"shop_default_language"`
|
||||
// Config config.Config `json:"config"`
|
||||
}
|
||||
|
||||
@@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
settings := SettingsResponse{
|
||||
App: AppSettings{
|
||||
Name: cfg.App.Name,
|
||||
Environment: cfg.App.Environment,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
|
||||
Name: cfg.App.Name,
|
||||
Environment: cfg.App.Environment,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
|
||||
CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID,
|
||||
ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE,
|
||||
// Config: *config.Get(),
|
||||
},
|
||||
Server: ServerSettings{
|
||||
|
||||
@@ -94,9 +94,9 @@ func (s *Server) Setup() error {
|
||||
productTranslation := s.restricted.Group("/product-translation")
|
||||
restricted.ProductTranslationHandlerRoutes(productTranslation)
|
||||
|
||||
// listing products routes (restricted)
|
||||
listProducts := s.restricted.Group("/list-products")
|
||||
restricted.ListProductsHandlerRoutes(listProducts)
|
||||
// lists of things routes (restricted)
|
||||
list := s.restricted.Group("/list")
|
||||
restricted.ListHandlerRoutes(list)
|
||||
|
||||
// locale selector (restricted)
|
||||
// this is basically for changing user's selected language and country
|
||||
@@ -115,6 +115,10 @@ func (s *Server) Setup() error {
|
||||
carts := s.restricted.Group("/carts")
|
||||
restricted.CartsHandlerRoutes(carts)
|
||||
|
||||
// storage (restricted)
|
||||
storage := s.restricted.Group("/storage")
|
||||
restricted.StorageHandlerRoutes(storage)
|
||||
|
||||
s.api.All("*", func(c fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
})
|
||||
|
||||
33
app/model/category.go
Normal file
33
app/model/category.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
type ScannedCategory struct {
|
||||
CategoryID uint `gorm:"column:category_id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Active uint `gorm:"column:active"`
|
||||
Position uint `gorm:"column:position"`
|
||||
ParentID uint `gorm:"column:id_parent"`
|
||||
IsRoot uint `gorm:"column:is_root_category"`
|
||||
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||
IsoCode string `gorm:"column:iso_code"`
|
||||
|
||||
Visited bool //this is for internal backend use only
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
CategoryID uint `json:"category_id" form:"category_id"`
|
||||
Label string `json:"label" form:"label"`
|
||||
// Active bool `json:"active" form:"active"`
|
||||
Params CategoryParams `json:"params" form:"params"`
|
||||
Children []Category `json:"children" form:"children"`
|
||||
}
|
||||
|
||||
type CategoryParams struct {
|
||||
CategoryID uint `json:"category_id" form:"category_id"`
|
||||
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
|
||||
Locale string `json:"locale" form:"locale"`
|
||||
}
|
||||
|
||||
type CategoryInBreadcrumb struct {
|
||||
CategoryID uint `json:"category_id" form:"category_id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
}
|
||||
@@ -1,31 +1,17 @@
|
||||
package model
|
||||
|
||||
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||
|
||||
// Represents a country together with its associated currency
|
||||
type Country struct {
|
||||
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
||||
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
|
||||
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"`
|
||||
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
||||
// PSCountryID int `gorm:"column:id_country" json:"ps_country_id"`
|
||||
// PSCountry *PSCountry `gorm:"foreignKey:PSCountryID;references:ID" json:"ps_country"`
|
||||
PSCurrencyID uint `gorm:"column:currency" json:"currency"`
|
||||
PSCurrency *PSCurrency `gorm:"foreignKey:PSCurrencyID;references:currency_id" json:"ps_currency"`
|
||||
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
||||
|
||||
PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"`
|
||||
PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"`
|
||||
}
|
||||
|
||||
func (Country) TableName() string {
|
||||
return "b2b_countries"
|
||||
}
|
||||
|
||||
type PSCountry struct {
|
||||
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
|
||||
}
|
||||
|
||||
func (PSCountry) TableName() string {
|
||||
return "ps_country"
|
||||
}
|
||||
|
||||
type PSCurrency struct {
|
||||
Currency int `gorm:"column:currency" json:"currency"`
|
||||
}
|
||||
|
||||
@@ -82,6 +82,15 @@ type UserSession struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UserLocale struct {
|
||||
// User is the Target user if present, otherwise same as Original.
|
||||
// User ought to be used in applications
|
||||
User *Customer
|
||||
// Original user is the one associated with auth token
|
||||
OriginalUser *Customer
|
||||
// Importantly, lang_id used in application is stored as OriginalUser.LangID
|
||||
}
|
||||
|
||||
// ToSession converts User to UserSession
|
||||
func (u *Customer) ToSession() *UserSession {
|
||||
return &UserSession{
|
||||
@@ -98,6 +107,7 @@ func (u *Customer) ToSession() *UserSession {
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" form:"email"`
|
||||
Password string `json:"password" form:"password"`
|
||||
LangID *uint `json:"lang_id" form:"lang_id"`
|
||||
}
|
||||
|
||||
// RegisterRequest represents the initial registration form data
|
||||
@@ -144,3 +154,11 @@ type RefreshToken struct {
|
||||
func (RefreshToken) TableName() string {
|
||||
return "b2b_refresh_tokens"
|
||||
}
|
||||
|
||||
type UserInList struct {
|
||||
UserID uint `gorm:"primaryKey;column:id" json:"user_id"`
|
||||
Email string `gorm:"column:email" json:"email"`
|
||||
FirstName string `gorm:"column:first_name" json:"first_name"`
|
||||
LastName string `gorm:"column:last_name" json:"last_name"`
|
||||
Role string `gorm:"column:role" json:"role"`
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ func (*PsGroupReduction) TableName() string {
|
||||
|
||||
var PsGroupReductionCols = struct {
|
||||
IDGroupReduction gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDCategory gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDCategory gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
}{
|
||||
IDGroupReduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group_reduction"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"),
|
||||
IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"),
|
||||
Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_group"),
|
||||
IDCategory: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "id_category"),
|
||||
Reduction: gormcol.Field{}.Set((&PsGroupReduction{}).TableName(), "reduction"),
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ func (*PsProductAttributeCombination) TableName() string {
|
||||
}
|
||||
|
||||
var PsProductAttributeCombinationCols = struct {
|
||||
IDAttribute gormcol.Field
|
||||
IDAttribute gormcol.Field
|
||||
IDProductAttribute gormcol.Field
|
||||
}{
|
||||
IDAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_attribute"),
|
||||
IDAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_attribute"),
|
||||
IDProductAttribute: gormcol.Field{}.Set((&PsProductAttributeCombination{}).TableName(), "id_product_attribute"),
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ func (*PsProductGroupReductionCache) TableName() string {
|
||||
|
||||
var PsProductGroupReductionCacheCols = struct {
|
||||
IDProduct gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
IDGroup gormcol.Field
|
||||
Reduction gormcol.Field
|
||||
}{
|
||||
IDProduct: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_product"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_group"),
|
||||
IDGroup: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "id_group"),
|
||||
Reduction: gormcol.Field{}.Set((&PsProductGroupReductionCache{}).TableName(), "reduction"),
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func (*PsPshowPshowproducttabsHook) TableName() string {
|
||||
}
|
||||
|
||||
var PsPshowPshowproducttabsHookCols = struct {
|
||||
IDHook gormcol.Field
|
||||
HookName gormcol.Field
|
||||
IDHook gormcol.Field
|
||||
HookName gormcol.Field
|
||||
PrestaIDHook gormcol.Field
|
||||
}{
|
||||
IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"),
|
||||
HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"),
|
||||
IDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "id_hook"),
|
||||
HookName: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "hook_name"),
|
||||
PrestaIDHook: gormcol.Field{}.Set((&PsPshowPshowproducttabsHook{}).TableName(), "presta_id_hook"),
|
||||
}
|
||||
|
||||
6
app/model/entry.go
Normal file
6
app/model/entry.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type EntryInList struct {
|
||||
Name string
|
||||
IsFolder bool
|
||||
}
|
||||
@@ -84,28 +84,4 @@ type ProductFilters struct {
|
||||
InStock uint `query:"stock,omitempty"`
|
||||
}
|
||||
|
||||
type ScannedCategory struct {
|
||||
CategoryID uint `gorm:"column:category_id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Active uint `gorm:"column:active"`
|
||||
Position uint `gorm:"column:position"`
|
||||
ParentID uint `gorm:"column:id_parent"`
|
||||
IsRoot uint `gorm:"column:is_root_category"`
|
||||
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||
IsoCode string `gorm:"column:iso_code"`
|
||||
}
|
||||
type Category struct {
|
||||
CategoryID uint `json:"category_id" form:"category_id"`
|
||||
Label string `json:"label" form:"label"`
|
||||
// Active bool `json:"active" form:"active"`
|
||||
Params CategpryParams `json:"params" form:"params"`
|
||||
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
|
||||
|
||||
@@ -18,7 +18,10 @@ type ProductDescription struct {
|
||||
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"`
|
||||
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"`
|
||||
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
||||
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
||||
Usage string `gorm:"column:_usage_;type:text" json:"usage" form:"usage"`
|
||||
|
||||
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
||||
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
|
||||
}
|
||||
|
||||
type ProductRow struct {
|
||||
|
||||
@@ -37,12 +37,11 @@ func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory,
|
||||
ps_category_lang.link_rewrite AS link_rewrite,
|
||||
ps_lang.iso_code AS iso_code
|
||||
`).
|
||||
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ? AND ??.id_lang = ?`,
|
||||
categoryLangTbl, categoryLangTbl, categoryTbl, categoryLangTbl, constdata.SHOP_ID, categoryLangTbl, idLang).
|
||||
Joins(`LEFT JOIN ? ON ??.id_category = ??.id_category AND ??.id_shop = ?`,
|
||||
categoryShopTbl, categoryShopTbl, categoryTbl, categoryShopTbl, constdata.SHOP_ID).
|
||||
Joins(`JOIN ? ON ??.id_lang = ??.id_lang`,
|
||||
langTbl, langTbl, categoryLangTbl).
|
||||
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
|
||||
constdata.SHOP_ID, idLang).
|
||||
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
|
||||
constdata.SHOP_ID).
|
||||
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
|
||||
Scan(&allCategories).Error
|
||||
|
||||
return allCategories, err
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package listProductsRepo
|
||||
package listRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
@@ -7,30 +7,32 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||
"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_marek/gormcol"
|
||||
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
||||
)
|
||||
|
||||
type UIListProductsRepo interface {
|
||||
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
type UIListRepo interface {
|
||||
ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||
ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error)
|
||||
}
|
||||
|
||||
type ListProductsRepo struct{}
|
||||
type ListRepo struct{}
|
||||
|
||||
func New() UIListProductsRepo {
|
||||
return &ListProductsRepo{}
|
||||
func New() UIListRepo {
|
||||
return &ListRepo{}
|
||||
}
|
||||
|
||||
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var listing []model.ProductInList
|
||||
func (repo *ListRepo) ListProducts(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var list []model.ProductInList
|
||||
var total int64
|
||||
|
||||
query := db.Get().
|
||||
Table("ps_product_shop ps").
|
||||
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
|
||||
Select(`
|
||||
ps.id_product AS product_id,
|
||||
pl.name AS name,
|
||||
pl.link_rewrite AS link_rewrite,
|
||||
CONCAT(?, ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
||||
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
||||
cl.name AS category_name,
|
||||
p.reference AS reference,
|
||||
COALESCE(v.variants_number, 0) AS variants_number,
|
||||
@@ -67,13 +69,53 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
|
||||
Order("ps.id_product DESC").
|
||||
Limit(p.Limit()).
|
||||
Offset(p.Offset()).
|
||||
Find(&listing).Error
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
return find.Found[model.ProductInList]{}, err
|
||||
}
|
||||
|
||||
return find.Found[model.ProductInList]{
|
||||
Items: listing,
|
||||
Items: list,
|
||||
Count: uint(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *ListRepo) ListUsers(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.UserInList], error) {
|
||||
var list []model.UserInList
|
||||
var total int64
|
||||
|
||||
query := db.Get().
|
||||
Table("b2b_customers AS users").
|
||||
Select(`
|
||||
users.id AS id,
|
||||
users.email AS email,
|
||||
users.first_name AS first_name,
|
||||
users.last_name AS last_name,
|
||||
users.role AS role
|
||||
`)
|
||||
|
||||
// Apply all filters
|
||||
if filt != nil {
|
||||
filt.ApplyAll(query)
|
||||
}
|
||||
|
||||
// run counter first as query is without limit and offset
|
||||
err := query.Count(&total).Error
|
||||
if err != nil {
|
||||
return find.Found[model.UserInList]{}, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("users.id DESC").
|
||||
Limit(p.Limit()).
|
||||
Offset(p.Offset()).
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
return find.Found[model.UserInList]{}, err
|
||||
}
|
||||
|
||||
return find.Found[model.UserInList]{
|
||||
Items: list,
|
||||
Count: uint(total),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package productDescriptionRepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UIProductDescriptionRepo interface {
|
||||
@@ -28,14 +31,42 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
||||
var ProductDescription model.ProductDescription
|
||||
|
||||
err := db.Get().
|
||||
Model(dbmodel.PsProductLang{}).
|
||||
Where(&dbmodel.PsProductLang{
|
||||
IDProduct: int32(productID),
|
||||
IDShop: int32(constdata.SHOP_ID),
|
||||
IDLang: int32(productid_lang),
|
||||
}).
|
||||
Select(`
|
||||
`+dbmodel.PsProductLangCols.IDProduct.TabCol()+` AS id_product,
|
||||
`+dbmodel.PsProductLangCols.IDShop.TabCol()+` AS id_shop,
|
||||
`+dbmodel.PsProductLangCols.IDLang.TabCol()+` AS id_lang,
|
||||
`+dbmodel.PsProductLangCols.Description.TabCol()+` AS description,
|
||||
`+dbmodel.PsProductLangCols.DescriptionShort.TabCol()+` AS description_short,
|
||||
`+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+` AS link_rewrite,
|
||||
`+dbmodel.PsProductLangCols.MetaDescription.TabCol()+` AS meta_description,
|
||||
`+dbmodel.PsProductLangCols.MetaKeywords.TabCol()+` AS meta_keywords,
|
||||
`+dbmodel.PsProductLangCols.MetaTitle.TabCol()+` AS meta_title,
|
||||
`+dbmodel.PsProductLangCols.Name.TabCol()+` AS name,
|
||||
`+dbmodel.PsProductLangCols.AvailableNow.TabCol()+` AS available_now,
|
||||
`+dbmodel.PsProductLangCols.AvailableLater.TabCol()+` AS available_later,
|
||||
`+dbmodel.PsProductLangCols.DeliveryInStock.TabCol()+` AS delivery_in_stock,
|
||||
`+dbmodel.PsProductLangCols.DeliveryOutStock.TabCol()+` AS delivery_out_stock,
|
||||
`+dbmodel.PsProductLangCols.Usage.TabCol()+` AS _usage_,
|
||||
CONCAT(?, '/', `+dbmodel.PsImageShopCols.IDImage.TabCol()+`, '-large_default/', `+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+`, '.webp') AS image_link
|
||||
`, config.Get().Image.ImagePrefix).
|
||||
Joins("JOIN " + dbmodel.TableNamePsImageShop +
|
||||
" ON " + dbmodel.PsImageShopCols.IDProduct.TabCol() + "=" + dbmodel.PsProductLangCols.IDProduct.TabCol() +
|
||||
" AND " + dbmodel.PsImageShopCols.Cover.TabCol() + " = 1").
|
||||
First(&ProductDescription).Error
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// handle "not found" case only
|
||||
ProductDescription.ExistsInDatabase = false
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("database error: %w", err)
|
||||
} else {
|
||||
ProductDescription.ExistsInDatabase = true
|
||||
}
|
||||
|
||||
return &ProductDescription, nil
|
||||
@@ -50,6 +81,7 @@ func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_
|
||||
}
|
||||
|
||||
err := db.Get().
|
||||
Model(dbmodel.PsProductLang{}).
|
||||
Where(&dbmodel.PsProductLang{
|
||||
IDProduct: int32(productID),
|
||||
IDShop: int32(constdata.SHOP_ID),
|
||||
|
||||
@@ -32,12 +32,12 @@ func New() UISearchRepo {
|
||||
}
|
||||
|
||||
func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) {
|
||||
url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MailiSearch.ServerURL, index)
|
||||
url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MeiliSearch.ServerURL, index)
|
||||
return r.doRequest(http.MethodPost, url, body)
|
||||
}
|
||||
|
||||
func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) {
|
||||
url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MailiSearch.ServerURL, index)
|
||||
url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MeiliSearch.ServerURL, index)
|
||||
return r.doRequest(http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if r.cfg.MailiSearch.ApiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey))
|
||||
if r.cfg.MeiliSearch.ApiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey))
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
100
app/repos/storageRepo/storageRepo.go
Normal file
100
app/repos/storageRepo/storageRepo.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package storageRepo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type UIStorageRepo interface {
|
||||
EntryInfo(abs_path string) (os.FileInfo, error)
|
||||
ListContent(abs_path string) (*[]model.EntryInList, error)
|
||||
Move(src_abs_path string, dest_abs_path string) error
|
||||
Copy(src_abs_path string, dest_abs_path string) error
|
||||
OpenFile(abs_path string) (*os.File, error)
|
||||
UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error
|
||||
CreateFolder(abs_path string) error
|
||||
DeleteFile(abs_path string) error
|
||||
DeleteFolder(abs_path string) error
|
||||
}
|
||||
|
||||
type StorageRepo struct{}
|
||||
|
||||
func New() UIStorageRepo {
|
||||
return &StorageRepo{}
|
||||
}
|
||||
|
||||
func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) {
|
||||
return os.Stat(abs_path)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) {
|
||||
entries, err := os.ReadDir(abs_path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entries_in_list []model.EntryInList
|
||||
|
||||
for _, entry := range entries {
|
||||
var next_entry_in_list model.EntryInList
|
||||
next_entry_in_list.Name = entry.Name()
|
||||
next_entry_in_list.IsFolder = entry.IsDir()
|
||||
|
||||
entries_in_list = append(entries_in_list, next_entry_in_list)
|
||||
}
|
||||
|
||||
return &entries_in_list, nil
|
||||
}
|
||||
|
||||
func (r *StorageRepo) Move(src_abs_path string, dest_abs_path string) error {
|
||||
return os.Rename(src_abs_path, dest_abs_path)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) Copy(src_abs_path string, dest_abs_path string) error {
|
||||
in, err := os.Open(src_abs_path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
info, err := in.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(dest_abs_path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Sync()
|
||||
}
|
||||
|
||||
func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) {
|
||||
return os.Open(abs_path)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error {
|
||||
return c.SaveFile(f, abs_path)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) CreateFolder(abs_path string) error {
|
||||
return os.Mkdir(abs_path, 0755)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) DeleteFile(abs_path string) error {
|
||||
return os.Remove(abs_path)
|
||||
}
|
||||
|
||||
func (r *StorageRepo) DeleteFolder(abs_path string) error {
|
||||
return os.RemoveAll(abs_path)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
@@ -14,13 +13,9 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"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/dlclark/regexp2"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
@@ -88,6 +83,15 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
|
||||
// Update last login time
|
||||
now := time.Now()
|
||||
user.LastLoginAt = &now
|
||||
|
||||
if req.LangID != nil {
|
||||
_, err := s.GetLangISOCode(*req.LangID)
|
||||
if err != nil {
|
||||
return nil, "", responseErrors.ErrBadLangID
|
||||
}
|
||||
user.LangID = *req.LangID
|
||||
}
|
||||
|
||||
s.db.Save(&user)
|
||||
|
||||
// Generate access token (JWT)
|
||||
@@ -167,7 +171,7 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
||||
baseURL := config.Get().App.BaseURL
|
||||
lang, err := s.GetLangISOCode(req.LangID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadLangID
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
|
||||
@@ -276,7 +280,7 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
|
||||
baseURL := config.Get().App.BaseURL
|
||||
lang, err := s.GetLangISOCode(user.LangID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadLangID
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
|
||||
@@ -482,7 +486,7 @@ func hashToken(raw string) string {
|
||||
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
|
||||
_, err := s.GetLangISOCode(user.LangID)
|
||||
if err != nil {
|
||||
return "", responseErrors.ErrBadLangID
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = s.CheckIfCountryExists(user.CountryID)
|
||||
@@ -508,97 +512,19 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
|
||||
return token.SignedString([]byte(s.config.JWTSecret))
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateJWTToken(c fiber.Ctx) error {
|
||||
// Get user ID from JWT claims in context (set by auth middleware)
|
||||
// claims, ok := c.Locals("jwt_claims").(*JWTClaims)
|
||||
// if !ok || claims == nil {
|
||||
// return c.Status(fiber.StatusUnauthorized).
|
||||
// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
|
||||
// }
|
||||
// fmt.Printf("claims: %v\n", claims)
|
||||
// var user model.Customer
|
||||
// // Find user by ID
|
||||
// if err := s.db.First(&user, claims.UserID).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
userLocals, ok := c.Locals(constdata.USER_LOCALES_NAME).(*model.UserSession)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
|
||||
}
|
||||
|
||||
user := model.Customer{
|
||||
ID: userLocals.UserID,
|
||||
Email: userLocals.Email,
|
||||
Role: userLocals.Role,
|
||||
LangID: userLocals.LangID,
|
||||
CountryID: userLocals.CountryID,
|
||||
IsActive: userLocals.IsActive,
|
||||
}
|
||||
|
||||
// Parse language and country_id from query params
|
||||
langIDStr := c.Query("lang_id")
|
||||
|
||||
var langID uint
|
||||
if langIDStr != "" {
|
||||
parsedID, err := strconv.ParseUint(langIDStr, 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadLangID)))
|
||||
}
|
||||
langID = uint(parsedID)
|
||||
|
||||
_, err = s.GetLangISOCode(langID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadLangID
|
||||
} else {
|
||||
user.LangID = langID
|
||||
}
|
||||
}
|
||||
|
||||
countryIDStr := c.Query("country_id")
|
||||
|
||||
var countryID uint
|
||||
if countryIDStr != "" {
|
||||
parsedID, err := strconv.ParseUint(countryIDStr, 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadCountryID)))
|
||||
}
|
||||
countryID = uint(parsedID)
|
||||
|
||||
err = s.CheckIfCountryExists(countryID)
|
||||
if err != nil {
|
||||
return responseErrors.ErrBadCountryID
|
||||
} else {
|
||||
user.CountryID = countryID
|
||||
}
|
||||
}
|
||||
|
||||
// Update choice and get new token using AuthService
|
||||
newToken, err := s.generateAccessToken(&user)
|
||||
func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
|
||||
// Update choice and get new access token using AuthService
|
||||
new_access_token, err := s.generateAccessToken(user)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Save the updated user
|
||||
if err := s.db.Save(&user).Error; err != nil {
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return "", fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
// Set the new JWT cookie
|
||||
cookie := new(fiber.Cookie)
|
||||
cookie.Name = "jwt_token"
|
||||
cookie.Value = newToken
|
||||
cookie.HTTPOnly = true
|
||||
cookie.Secure = true
|
||||
cookie.SameSite = fiber.CookieSameSiteLaxMode
|
||||
|
||||
c.Cookie(cookie)
|
||||
|
||||
return c.JSON(response.Make(&fiber.Map{"token": newToken}, 0, i18n.T_(c, response.Message_OK)))
|
||||
return new_access_token, nil
|
||||
}
|
||||
|
||||
// generateVerificationToken generates a random verification token
|
||||
@@ -623,14 +549,20 @@ func validatePassword(password string) error {
|
||||
|
||||
func (s *AuthService) GetLangISOCode(langID uint) (string, error) {
|
||||
var lang string
|
||||
var err error
|
||||
|
||||
if langID == 0 { // retrieve the default lang
|
||||
err := db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
return lang, err
|
||||
err = db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
} else {
|
||||
err := db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
return lang, err
|
||||
err = db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return lang, err
|
||||
} else if lang == "" {
|
||||
return lang, responseErrors.ErrBadLangID
|
||||
}
|
||||
return lang, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) CheckIfCountryExists(countryID uint) error {
|
||||
|
||||
@@ -153,7 +153,8 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
||||
Role: model.RoleUser,
|
||||
IsActive: true,
|
||||
EmailVerified: true,
|
||||
LangID: 2,
|
||||
LangID: 2, // default is english
|
||||
CountryID: 2, // default is England
|
||||
}
|
||||
|
||||
if err := s.db.Create(&newUser).Error; err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/templ/emails"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/view"
|
||||
)
|
||||
@@ -133,6 +134,6 @@ func (s *EmailService) passwordResetEmailTemplate(name, resetURL string, langID
|
||||
// newUserAdminNotificationTemplate returns the HTML template for admin notification
|
||||
func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, baseURL string) string {
|
||||
buf := bytes.Buffer{}
|
||||
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: 2, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
|
||||
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type ListProductsService struct {
|
||||
listProductsRepo listProductsRepo.UIListProductsRepo
|
||||
}
|
||||
|
||||
func New() *ListProductsService {
|
||||
return &ListProductsService{
|
||||
listProductsRepo: listProductsRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
var products find.Found[model.ProductInList]
|
||||
|
||||
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
|
||||
if err != nil {
|
||||
return products, err
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
26
app/service/listService/listService.go
Normal file
26
app/service/listService/listService.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package listService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/listRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type ListService struct {
|
||||
listRepo listRepo.UIListRepo
|
||||
}
|
||||
|
||||
func New() *ListService {
|
||||
return &ListService{
|
||||
listRepo: listRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ListService) ListProducts(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||
return s.listRepo.ListProducts(id_lang, p, filters)
|
||||
}
|
||||
|
||||
func (s *ListService) ListUsers(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.UserInList], error) {
|
||||
return s.listRepo.ListUsers(id_lang, p, filters)
|
||||
}
|
||||
@@ -27,8 +27,8 @@ type MeiliService struct {
|
||||
func New() *MeiliService {
|
||||
|
||||
client := meilisearch.New(
|
||||
config.Get().MailiSearch.ServerURL,
|
||||
meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey),
|
||||
config.Get().MeiliSearch.ServerURL,
|
||||
meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey),
|
||||
)
|
||||
|
||||
return &MeiliService{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package menuService
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
@@ -21,7 +22,7 @@ func New() *MenuService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) {
|
||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
|
||||
if err != nil {
|
||||
return &model.Category{}, err
|
||||
@@ -31,7 +32,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
root_index := 0
|
||||
root_found := false
|
||||
for i := 0; i < len(all_categories); i++ {
|
||||
if all_categories[i].IsRoot == 1 {
|
||||
if all_categories[i].CategoryID == root_category_id {
|
||||
root_index = i
|
||||
root_found = true
|
||||
break
|
||||
@@ -44,6 +45,7 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
// now create the children and reorder them according to position
|
||||
id_to_index := make(map[uint]int)
|
||||
for i := 0; i < len(all_categories); i++ {
|
||||
all_categories[i].Visited = false
|
||||
id_to_index[all_categories[i].CategoryID] = i
|
||||
}
|
||||
|
||||
@@ -58,19 +60,32 @@ func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||
}
|
||||
|
||||
// finally, create the tree
|
||||
tree := s.createTree(root_index, &all_categories, &children_indices)
|
||||
tree, success := s.createTree(root_index, &all_categories, &children_indices)
|
||||
if !success {
|
||||
return &tree, responseErrors.ErrCircularDependency
|
||||
}
|
||||
|
||||
return &tree, nil
|
||||
}
|
||||
|
||||
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
|
||||
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) (model.Category, bool) {
|
||||
node := s.scannedToNormalCategory((*all_categories)[index])
|
||||
|
||||
if (*all_categories)[index].Visited {
|
||||
return node, false
|
||||
}
|
||||
(*all_categories)[index].Visited = true
|
||||
|
||||
for i := 0; i < len((*children_indices)[index]); i++ {
|
||||
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
|
||||
next_child, success := s.createTree((*children_indices)[index][i].Index, all_categories, children_indices)
|
||||
if !success {
|
||||
return node, false
|
||||
}
|
||||
node.Children = append(node.Children, next_child)
|
||||
}
|
||||
|
||||
return node
|
||||
(*all_categories)[index].Visited = false // just in case we have a "diamond" diagram
|
||||
return node, true
|
||||
}
|
||||
|
||||
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
|
||||
@@ -83,7 +98,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
|
||||
normal.CategoryID = scanned.CategoryID
|
||||
normal.Label = scanned.Name
|
||||
// normal.Active = scanned.Active == 1
|
||||
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
|
||||
normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
|
||||
normal.Children = []model.Category{}
|
||||
return normal
|
||||
}
|
||||
@@ -98,6 +113,69 @@ func (a ByPosition) Len() int { return len(a) }
|
||||
func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
|
||||
|
||||
func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) {
|
||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
|
||||
if err != nil {
|
||||
return []model.CategoryInBreadcrumb{}, err
|
||||
}
|
||||
|
||||
breadcrumb := []model.CategoryInBreadcrumb{}
|
||||
|
||||
start_index := 0
|
||||
start_found := false
|
||||
for i := 0; i < len(all_categories); i++ {
|
||||
if all_categories[i].CategoryID == start_category_id {
|
||||
start_index = i
|
||||
start_found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !start_found {
|
||||
return []model.CategoryInBreadcrumb{}, responseErrors.ErrStartCategoryNotFound
|
||||
}
|
||||
|
||||
// map category ids to indices
|
||||
id_to_index := make(map[uint]int)
|
||||
for i := 0; i < len(all_categories); i++ {
|
||||
all_categories[i].Visited = false
|
||||
id_to_index[all_categories[i].CategoryID] = i
|
||||
}
|
||||
|
||||
// do a simple graph traversal, always jumping from node to its parent
|
||||
index := start_index
|
||||
success := true
|
||||
for {
|
||||
if all_categories[index].Visited {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
all_categories[index].Visited = true
|
||||
|
||||
var next_category model.CategoryInBreadcrumb
|
||||
next_category.CategoryID = all_categories[index].CategoryID
|
||||
next_category.Name = all_categories[index].Name
|
||||
breadcrumb = append(breadcrumb, next_category)
|
||||
|
||||
if all_categories[index].CategoryID == root_category_id {
|
||||
break
|
||||
}
|
||||
next_index, ok := id_to_index[all_categories[index].ParentID]
|
||||
if !ok {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
index = next_index
|
||||
}
|
||||
|
||||
slices.Reverse(breadcrumb)
|
||||
|
||||
if !success {
|
||||
return breadcrumb, responseErrors.ErrRootNeverReached
|
||||
}
|
||||
|
||||
return breadcrumb, nil
|
||||
}
|
||||
|
||||
func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) {
|
||||
items, err := s.routesRepo.GetTopMenu(id)
|
||||
if err != nil {
|
||||
|
||||
163
app/service/storageService/storageService.go
Normal file
163
app/service/storageService/storageService.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package storageService
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type StorageService struct {
|
||||
storageRepo storageRepo.UIStorageRepo
|
||||
}
|
||||
|
||||
func New() *StorageService {
|
||||
return &StorageService{
|
||||
storageRepo: storageRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil, responseErrors.ErrFolderDoesNotExist
|
||||
}
|
||||
|
||||
entries_in_list, err := s.storageRepo.ListContent(abs_path)
|
||||
return entries_in_list, err
|
||||
}
|
||||
|
||||
func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || info.IsDir() {
|
||||
return nil, "", 0, responseErrors.ErrFileDoesNotExist
|
||||
}
|
||||
|
||||
f, err := s.storageRepo.OpenFile(abs_path)
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
}
|
||||
|
||||
return f, filepath.Base(abs_path), info.Size(), nil
|
||||
}
|
||||
|
||||
func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error {
|
||||
_, err := s.storageRepo.EntryInfo(src_abs_path)
|
||||
if err != nil {
|
||||
return responseErrors.ErrFileDoesNotExist
|
||||
}
|
||||
|
||||
_, err = s.storageRepo.EntryInfo(dest_abs_path)
|
||||
if err == nil {
|
||||
return responseErrors.ErrNameTaken
|
||||
} else if os.IsNotExist(err) {
|
||||
return s.storageRepo.Move(src_abs_path, dest_abs_path)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error {
|
||||
_, err := s.storageRepo.EntryInfo(src_abs_path)
|
||||
if err != nil {
|
||||
return responseErrors.ErrFileDoesNotExist
|
||||
}
|
||||
|
||||
_, err = s.storageRepo.EntryInfo(dest_abs_path)
|
||||
if err == nil {
|
||||
return responseErrors.ErrNameTaken
|
||||
} else if os.IsNotExist(err) {
|
||||
return s.storageRepo.Copy(src_abs_path, dest_abs_path)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageService) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return responseErrors.ErrFolderDoesNotExist
|
||||
}
|
||||
|
||||
name := f.Filename
|
||||
if name == "" || name == "." || name == ".." || filepath.Base(name) != name {
|
||||
return responseErrors.ErrBadAttribute
|
||||
}
|
||||
abs_file_path, err := s.AbsPath(abs_path, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if abs_file_path == abs_path {
|
||||
return responseErrors.ErrBadAttribute
|
||||
}
|
||||
|
||||
info, err = s.storageRepo.EntryInfo(abs_file_path)
|
||||
if err == nil {
|
||||
return responseErrors.ErrNameTaken
|
||||
} else if os.IsNotExist(err) {
|
||||
return s.storageRepo.UploadFile(c, abs_file_path, f)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageService) CreateFolder(abs_path string, name string) error {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return responseErrors.ErrFolderDoesNotExist
|
||||
}
|
||||
|
||||
if name == "" || name == "." || name == ".." || filepath.Base(name) != name {
|
||||
return responseErrors.ErrBadAttribute
|
||||
}
|
||||
abs_folder_path, err := s.AbsPath(abs_path, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if abs_folder_path == abs_path {
|
||||
return responseErrors.ErrBadAttribute
|
||||
}
|
||||
|
||||
info, err = s.storageRepo.EntryInfo(abs_folder_path)
|
||||
if err == nil {
|
||||
return responseErrors.ErrNameTaken
|
||||
} else if os.IsNotExist(err) {
|
||||
return s.storageRepo.CreateFolder(abs_folder_path)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageService) DeleteFile(abs_path string) error {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || info.IsDir() {
|
||||
return responseErrors.ErrFileDoesNotExist
|
||||
}
|
||||
|
||||
return s.storageRepo.DeleteFile(abs_path)
|
||||
}
|
||||
|
||||
func (s *StorageService) DeleteFolder(abs_path string) error {
|
||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return responseErrors.ErrFolderDoesNotExist
|
||||
}
|
||||
|
||||
return s.storageRepo.DeleteFolder(abs_path)
|
||||
}
|
||||
|
||||
// AbsPath extracts an absolute path and validates it
|
||||
func (s *StorageService) AbsPath(root string, relativePath string) (string, error) {
|
||||
clean_name := filepath.Clean(relativePath)
|
||||
full_path := filepath.Join(root, clean_name)
|
||||
|
||||
if full_path != root && !strings.HasPrefix(full_path, root+string(os.PathSeparator)) {
|
||||
return "", responseErrors.ErrAccessDenied
|
||||
}
|
||||
|
||||
return full_path, nil
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package constdata
|
||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||
const SHOP_ID = 1
|
||||
const SHOP_DEFAULT_LANGUAGE = 1
|
||||
const ADMIN_NOTIFICATION_LANGUAGE = 2
|
||||
|
||||
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
|
||||
const CATEGORY_TREE_ROOT_ID = 2
|
||||
|
||||
const MAX_AMOUNT_OF_CARTS_PER_USER = 10
|
||||
const DEFAULT_NEW_CART_NAME = "new cart"
|
||||
|
||||
const USER_LOCALES_NAME = "user"
|
||||
const USER_LOCALES_ID = "userID"
|
||||
const USER_LOCALE = "user"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
@@ -177,7 +178,7 @@ func (s *TranslationsStore) ReloadTranslations(translations []model.Translation)
|
||||
|
||||
// T_ is meant to be used to translate error messages and other system communicates.
|
||||
func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string {
|
||||
if langID, ok := c.Locals("langID").(uint); ok {
|
||||
if langID, ok := localeExtractor.GetLangID(c); ok {
|
||||
parts := strings.Split(string(key), ".")
|
||||
|
||||
if len(parts) >= 2 {
|
||||
|
||||
31
app/utils/localeExtractor/localeExtractor.go
Normal file
31
app/utils/localeExtractor/localeExtractor.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package localeExtractor
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func GetLangID(c fiber.Ctx) (uint, bool) {
|
||||
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok || user_locale.OriginalUser == nil {
|
||||
return 0, false
|
||||
}
|
||||
return user_locale.OriginalUser.LangID, true
|
||||
}
|
||||
|
||||
func GetUserID(c fiber.Ctx) (uint, bool) {
|
||||
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok || user_locale.User == nil {
|
||||
return 0, false
|
||||
}
|
||||
return user_locale.User.ID, true
|
||||
}
|
||||
|
||||
func GetOriginalUserRole(c fiber.Ctx) (model.CustomerRole, bool) {
|
||||
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
|
||||
if !ok || user_locale.OriginalUser == nil {
|
||||
return "", false
|
||||
}
|
||||
return user_locale.OriginalUser.Role, true
|
||||
}
|
||||
@@ -50,12 +50,22 @@ var (
|
||||
ErrBadPaging = errors.New("bad or missing paging attribute value in header")
|
||||
|
||||
// Typed errors for menu handler
|
||||
ErrNoRootFound = errors.New("no root found in categories table")
|
||||
ErrNoRootFound = errors.New("no root found in categories table")
|
||||
ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)")
|
||||
ErrStartCategoryNotFound = errors.New("the start category has not been found")
|
||||
ErrRootNeverReached = errors.New("the root category is not an ancestor of start category")
|
||||
|
||||
// Typed errors for carts handler
|
||||
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
|
||||
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
|
||||
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
|
||||
|
||||
// Typed errors for storage
|
||||
ErrAccessDenied = errors.New("access denied!")
|
||||
ErrFolderDoesNotExist = errors.New("folder does not exist")
|
||||
ErrFileDoesNotExist = errors.New("file does not exist")
|
||||
ErrNameTaken = errors.New("name taken")
|
||||
ErrMissingFileFieldDocument = errors.New("missing file field 'document'")
|
||||
)
|
||||
|
||||
// Error represents an error with HTTP status code
|
||||
@@ -145,6 +155,12 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
|
||||
case errors.Is(err, ErrNoRootFound):
|
||||
return i18n.T_(c, "error.no_root_found")
|
||||
case errors.Is(err, ErrCircularDependency):
|
||||
return i18n.T_(c, "error.circular_dependency")
|
||||
case errors.Is(err, ErrStartCategoryNotFound):
|
||||
return i18n.T_(c, "error.start_category_not_found")
|
||||
case errors.Is(err, ErrRootNeverReached):
|
||||
return i18n.T_(c, "error.root_never_reached")
|
||||
|
||||
case errors.Is(err, ErrMaxAmtOfCartsReached):
|
||||
return i18n.T_(c, "error.max_amt_of_carts_reached")
|
||||
@@ -153,6 +169,17 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||
return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
|
||||
|
||||
case errors.Is(err, ErrAccessDenied):
|
||||
return i18n.T_(c, "error.access_denied")
|
||||
case errors.Is(err, ErrFolderDoesNotExist):
|
||||
return i18n.T_(c, "error.folder_does_not_exist")
|
||||
case errors.Is(err, ErrFileDoesNotExist):
|
||||
return i18n.T_(c, "error.file_does_not_exist")
|
||||
case errors.Is(err, ErrNameTaken):
|
||||
return i18n.T_(c, "error.name_taken")
|
||||
case errors.Is(err, ErrMissingFileFieldDocument):
|
||||
return i18n.T_(c, "error.missing_file_field_document")
|
||||
|
||||
default:
|
||||
return i18n.T_(c, "error.err_internal_server_error")
|
||||
}
|
||||
@@ -189,9 +216,17 @@ func GetErrorStatus(err error) int {
|
||||
errors.Is(err, ErrInvalidXHTML),
|
||||
errors.Is(err, ErrBadPaging),
|
||||
errors.Is(err, ErrNoRootFound),
|
||||
errors.Is(err, ErrCircularDependency),
|
||||
errors.Is(err, ErrStartCategoryNotFound),
|
||||
errors.Is(err, ErrRootNeverReached),
|
||||
errors.Is(err, ErrMaxAmtOfCartsReached),
|
||||
errors.Is(err, ErrUserHasNoSuchCart),
|
||||
errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||
errors.Is(err, ErrProductOrItsVariationDoesNotExist),
|
||||
errors.Is(err, ErrAccessDenied),
|
||||
errors.Is(err, ErrFolderDoesNotExist),
|
||||
errors.Is(err, ErrFileDoesNotExist),
|
||||
errors.Is(err, ErrNameTaken),
|
||||
errors.Is(err, ErrMissingFileFieldDocument):
|
||||
return fiber.StatusBadRequest
|
||||
case errors.Is(err, ErrEmailExists):
|
||||
return fiber.StatusConflict
|
||||
|
||||
1
bo/.gitignore
vendored
1
bo/.gitignore
vendored
@@ -13,6 +13,7 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
/bo/components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
13
bo/components.d.ts
vendored
13
bo/components.d.ts
vendored
@@ -11,33 +11,26 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
|
||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
|
||||
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
||||
copy: typeof import('./src/components/inner/categoryMenu copy.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']
|
||||
Page: typeof import('./src/components/customer/Page.vue')['default']
|
||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||
PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default']
|
||||
PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default']
|
||||
PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
|
||||
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
|
||||
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
|
||||
PageProductsList: typeof import('./src/components/admin/PageProductsList.vue')['default']
|
||||
PageProfileDetails: typeof import('./src/components/customer/PageProfileDetails.vue')['default']
|
||||
PageProfileDetailsAddInfo: typeof import('./src/components/customer/PageProfileDetailsAddInfo.vue')['default']
|
||||
PageStatistic: typeof import('./src/components/customer/PageStatistic.vue')['default']
|
||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
||||
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
|
||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
@@ -49,13 +42,11 @@ declare module 'vue' {
|
||||
UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
|
||||
UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
|
||||
UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UDropdownMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
|
||||
UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default']
|
||||
UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||
UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
||||
ULink: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/overrides/vue-router/Link.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']
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import LangSwitch from './inner/langSwitch.vue'
|
||||
import ThemeSwitch from './inner/themeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { currentLang } from '@/router/langs'
|
||||
import type { LabelTrans, TopMenuItem } from '@/types'
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
let menu = ref()
|
||||
@@ -19,30 +20,43 @@ async function getTopMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const menuItems = computed(() => transformMenu(menu.value[0].children, currentLang.value?.iso_code))
|
||||
function transformMenu(items: TopMenuItem[], locale: string | undefined): NavigationMenuItem[] {
|
||||
|
||||
return items.map((item) => {
|
||||
let route = {
|
||||
icon: 'i-lucide-house',
|
||||
const route: NavigationMenuItem = {
|
||||
icon: item.label.icon ? item.label.icon : 'i-lucide-house',
|
||||
label: item.label.trans[locale as keyof LabelTrans].label,
|
||||
children: item.children ? transformMenu(item.children, locale) : undefined,
|
||||
children: item.children
|
||||
? transformMenu(item.children, locale)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
route.onSelect = () => {
|
||||
const query = {
|
||||
name: item.params.route.name,
|
||||
params: {
|
||||
...(item.params.route.params || {}),
|
||||
locale: currentLang.value?.iso_code
|
||||
}
|
||||
}
|
||||
|
||||
router.push(query)
|
||||
}
|
||||
if (item.params?.route) {
|
||||
route = { ...route, ...{ to: { name: item.params.route.name, params: { locale: locale } } } }
|
||||
}
|
||||
|
||||
return route
|
||||
})
|
||||
}
|
||||
|
||||
await getTopMenu()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ menuItems }}
|
||||
<!-- fixed top-0 left-0 right-0 z-50 -->
|
||||
<header class="bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<!-- px-4 sm:px-6 lg:px-8 -->
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-14">
|
||||
<!-- Logo -->
|
||||
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
|
||||
@@ -52,49 +66,18 @@ await getTopMenu()
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
</RouterLink>
|
||||
|
||||
<UNavigationMenu :items="menuItems" class="w-full" />
|
||||
|
||||
<!-- {{ router }} -->
|
||||
<!-- <RouterLink :to="{ name: 'admin-products' }">
|
||||
products list
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'admin-product-detail' }">
|
||||
product detail
|
||||
</RouterLink>
|
||||
<RouterLink :to="{
|
||||
name: 'customer-product', params: {
|
||||
product_id: '51'
|
||||
}
|
||||
}">
|
||||
Product (51)
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'addresses' }">
|
||||
Addresses
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'profile-details' }">
|
||||
Customer Data
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'cart' }">
|
||||
Cart
|
||||
</RouterLink>
|
||||
<RouterLink :to="{
|
||||
name: 'cart-details', params: {
|
||||
cart_id: '1'
|
||||
}
|
||||
}">
|
||||
Cart details (1)
|
||||
</RouterLink> -->
|
||||
<UNavigationMenu :type="'trigger'" :ui="{
|
||||
root: 'justify-center',
|
||||
list: 'gap-4'
|
||||
}" :items="menuItems" class="w-full"></UNavigationMenu>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
<LangSwitch />
|
||||
<!-- Theme Switcher -->
|
||||
<ThemeSwitch />
|
||||
<!-- Logout Button (only when authenticated) -->
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
@click="authStore.logout()"
|
||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)"
|
||||
>
|
||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('general.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
<template>
|
||||
<suspense>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
|
||||
<template #item="{ item, active }">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<UIcon name="i-heroicons-book-open" />
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</UNavigationMenu> -->
|
||||
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="flex gap-2">
|
||||
<CategoryMenuListing />
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||
thead: 'hidden'
|
||||
}" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||
</div>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
<div class="flex gap-10">
|
||||
<CategoryMenu />
|
||||
<div class="w-full flex flex-col items-center gap-4">
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
|
||||
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
@@ -46,26 +18,20 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
||||
import type { Product } from '@/types/product'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const perPage = ref(15)
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page) || 1,
|
||||
get: () => Number(route.query.p) || 1,
|
||||
set: (val: number) => {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
page: val
|
||||
p: val
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -101,7 +67,6 @@ const sortField = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
@@ -162,17 +127,25 @@ async function fetchProductList() {
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
if (value === undefined || value === null) return
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => params.append(key, String(v)))
|
||||
} else {
|
||||
params.append(key, String(value))
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/list-products/get-listing?${params}`
|
||||
if (route.params.category_id)
|
||||
params.append('category_id', String(route.params.category_id))
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}`
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
total.value = Number(response.count) || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
@@ -182,16 +155,11 @@ async function fetchProductList() {
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
name: 'customer-product-details',
|
||||
params: { product_id: productId }
|
||||
})
|
||||
}
|
||||
|
||||
const selectedCount = ref({
|
||||
product_id: null,
|
||||
count: 0
|
||||
})
|
||||
|
||||
function getIcon(name: string) {
|
||||
if (sortField.value[0] === name) {
|
||||
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
||||
@@ -205,25 +173,7 @@ const UInput = resolveComponent('UInput')
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
{
|
||||
id: 'expand',
|
||||
cell: ({ row }) =>
|
||||
h(UButton, {
|
||||
color: 'neutral',
|
||||
variant: 'ghost',
|
||||
icon: 'i-lucide-chevron-down',
|
||||
square: true,
|
||||
'aria-label': 'Expand',
|
||||
ui: {
|
||||
leadingIcon: [
|
||||
'transition-transform',
|
||||
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
|
||||
]
|
||||
},
|
||||
onClick: () => row.toggleExpanded()
|
||||
})
|
||||
},
|
||||
const columns: TableColumn<Product>[] = [
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: ({ column }) => {
|
||||
@@ -314,108 +264,17 @@ const columns: TableColumn<Payment>[] = [
|
||||
},
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: 'Count',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
color: 'primary',
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const columnsChild: TableColumn<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: '',
|
||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'image_link',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image_link') as string,
|
||||
style: 'width:40px;height:40px;object-fit:cover;'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('name') as string
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
}, () => 'Show product')
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -427,4 +286,4 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,126 +1,130 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container my-10 mx-auto ">
|
||||
<div class="container my-10 mx-auto ">
|
||||
|
||||
<div
|
||||
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
||||
<div class="flex items-end gap-3">
|
||||
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!" valueKey="iso_code">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
|
||||
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
|
||||
modelValue)?.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
||||
<span class="text-md">{{ item.flag }}</span>
|
||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</USelect>
|
||||
<div
|
||||
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
||||
<div class="flex items-end gap-3">
|
||||
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!"
|
||||
valueKey="iso_code">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
|
||||
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
|
||||
modelValue)?.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
||||
<span class="text-md">{{ item.flag }}</span>
|
||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</USelect>
|
||||
</div>
|
||||
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
||||
Translate
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
||||
Translate
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
|
||||
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
|
||||
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
|
||||
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
|
||||
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
|
||||
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
|
||||
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
|
||||
</div>
|
||||
<div v-else-if="productStore.error" class="flex items-center justify-center py-20">
|
||||
<p class="text-red-500">{{ productStore.error }}</p>
|
||||
</div>
|
||||
<div v-else-if="productStore.productDescription" class="flex items-start gap-30">
|
||||
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
|
||||
<div v-else-if="productStore.error" class="flex items-center justify-center py-20">
|
||||
<p class="text-red-500">{{ productStore.error }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-[25px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.name || 'Product Name' }}
|
||||
</p>
|
||||
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
|
||||
<div class="space-y-[10px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" />
|
||||
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
|
||||
{{ productStore.productDescription.available_now }}
|
||||
</p>
|
||||
<div v-else-if="productStore.productDescription" class="flex items-start gap-30">
|
||||
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-[25px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.name || 'Product Name' }}
|
||||
</p>
|
||||
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
|
||||
<div class="space-y-[10px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" />
|
||||
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
|
||||
{{ productStore.productDescription.available_now }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" />
|
||||
<p class="text-[18px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" />
|
||||
<p class="text-[18px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="productStore.productDescription" class="mt-16">
|
||||
<div class="flex gap-4 my-6">
|
||||
<UButton @click="activeTab = 'description'"
|
||||
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
|
||||
variant="outline">
|
||||
<p class="dark:text-white">Description</p>
|
||||
</UButton>
|
||||
|
||||
<UButton @click="activeTab = 'usage'"
|
||||
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
|
||||
variant="outline">
|
||||
<p class="dark:text-white">Usage</p>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'usage'"
|
||||
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
|
||||
|
||||
<div class="flex justify-end items-center gap-3 mb-4">
|
||||
<UButton v-if="!isEditing" @click="enableEdit"
|
||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
||||
<p class="text-white">Change Text</p>
|
||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
|
||||
</UButton>
|
||||
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
<p class="dark:text-white text-black">Save the edited text</p>
|
||||
</UButton>
|
||||
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline"
|
||||
class="p-2.5 cursor-pointer">
|
||||
Cancel
|
||||
</UButton>
|
||||
</div>
|
||||
<p ref="usageRef" v-html="productStore.productDescription.usage"
|
||||
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'description'"
|
||||
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
|
||||
<div class="flex items-center justify-end gap-3 mb-4">
|
||||
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
|
||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
||||
<p class="text-white">Change Text</p>
|
||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
|
||||
</UButton>
|
||||
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline"
|
||||
class="p-2.5 cursor-pointer">
|
||||
<p class="dark:text-white text-black ">Save the edited text</p>
|
||||
</UButton>
|
||||
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral"
|
||||
variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
|
||||
</div>
|
||||
<div ref="descriptionRef" v-html="productStore.productDescription.description"
|
||||
class="flex flex-col justify-center dark:text-white text-black">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="productStore.productDescription" class="mt-16">
|
||||
<div class="flex gap-4 my-6">
|
||||
<UButton @click="activeTab = 'description'"
|
||||
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
|
||||
variant="outline">
|
||||
<p class="dark:text-white">Description</p>
|
||||
</UButton>
|
||||
|
||||
<UButton @click="activeTab = 'usage'"
|
||||
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
|
||||
variant="outline">
|
||||
<p class="dark:text-white">Usage</p>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'usage'"
|
||||
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
|
||||
|
||||
<div class="flex justify-end items-center gap-3 mb-4">
|
||||
<UButton v-if="!isEditing" @click="enableEdit"
|
||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
||||
<p class="text-white">Change Text</p>
|
||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
|
||||
</UButton>
|
||||
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
<p class="dark:text-white text-black">Save the edited text</p>
|
||||
</UButton>
|
||||
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
Cancel
|
||||
</UButton>
|
||||
</div>
|
||||
<p ref="usageRef" v-html="productStore.productDescription.usage"
|
||||
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'description'"
|
||||
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
|
||||
<div class="flex items-center justify-end gap-3 mb-4">
|
||||
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
|
||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
||||
<p class="text-white">Change Text</p>
|
||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
|
||||
</UButton>
|
||||
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
<p class="dark:text-white text-black ">Save the edited text</p>
|
||||
</UButton>
|
||||
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral" variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
|
||||
</div>
|
||||
<div ref="descriptionRef" v-html="productStore.productDescription.description"
|
||||
class="flex flex-col justify-center dark:text-white text-black">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -142,7 +146,7 @@ const isEditing = ref(false)
|
||||
|
||||
const availableLangs = computed(() => langs)
|
||||
|
||||
const selectedLanguage = ref('pl')
|
||||
const selectedLanguage = ref('en')
|
||||
|
||||
const currentLangId = ref(2)
|
||||
const productID = ref<number>(0)
|
||||
@@ -176,7 +180,7 @@ const translateToSelectedLanguage = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id
|
||||
const id = route.params.product_id
|
||||
if (id) {
|
||||
productID.value = Number(id)
|
||||
await fetchForLanguage(selectedLanguage.value)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<div class="container mx-auto">
|
||||
<h2
|
||||
class="font-semibold text-black dark:text-white pb-6 text-2xl">
|
||||
{{ t('Cart Items') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<div class="container mx-auto">
|
||||
<div class="flex flex-col gap-5 mb-6">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
||||
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
|
||||
<div class="container mx-auto flex flex-col gap-5 md:gap-10">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
|
||||
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
|
||||
<div class="flex-1">
|
||||
|
||||
9
bo/src/components/customer/PageOrders.vue
Normal file
9
bo/src/components/customer/PageOrders.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
Orders page
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Default from '@/layouts/default.vue'
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mt-20 mx-auto">
|
||||
<div class="container mx-auto">
|
||||
<div class="flex md:flex-row flex-col justify-between gap-8 my-6">
|
||||
<div class="flex-1">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||
|
||||
430
bo/src/components/customer/PageProducts.vue
Normal file
430
bo/src/components/customer/PageProducts.vue
Normal file
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<suspense>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto">
|
||||
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
|
||||
<template #item="{ item, active }">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<UIcon name="i-heroicons-book-open" />
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</UNavigationMenu> -->
|
||||
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="flex gap-2">
|
||||
<CategoryMenu />
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||
thead: 'hidden'
|
||||
}" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||
</div>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</suspense>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, h, resolveComponent, computed } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page) || 1,
|
||||
set: (val: number) => {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
page: val
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const sortField = computed({
|
||||
get: () => [
|
||||
route.query.sort as string | undefined,
|
||||
route.query.direction as 'asc' | 'desc' | undefined
|
||||
],
|
||||
|
||||
set: ([sort]: [string, 'asc' | 'desc']) => {
|
||||
const currentSort = route.query.sort as string | undefined
|
||||
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
|
||||
|
||||
let query = { ...route.query }
|
||||
|
||||
if (currentSort === sort) {
|
||||
if (currentDirection === 'asc') {
|
||||
query.direction = 'desc'
|
||||
} else if (currentDirection === 'desc') {
|
||||
delete query.sort
|
||||
delete query.direction
|
||||
} else {
|
||||
query.direction = 'asc'
|
||||
query.sort = sort
|
||||
}
|
||||
} else {
|
||||
query.sort = sort
|
||||
query.direction = 'asc'
|
||||
}
|
||||
|
||||
router.push({ query })
|
||||
}
|
||||
})
|
||||
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = computed<Record<string, string>>({
|
||||
get: () => {
|
||||
const q = { ...route.query }
|
||||
delete q.page
|
||||
delete q.sort
|
||||
delete q.direction
|
||||
return q as Record<string, string>
|
||||
},
|
||||
set: (val) => {
|
||||
const baseQuery = { ...route.query }
|
||||
|
||||
Object.keys(baseQuery).forEach(key => {
|
||||
if (!['page', 'sort', 'direction'].includes(key)) {
|
||||
delete baseQuery[key]
|
||||
}
|
||||
})
|
||||
|
||||
router.push({
|
||||
query: {
|
||||
...baseQuery,
|
||||
...val,
|
||||
page: 1
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function debounce(fn: Function, delay = 400) {
|
||||
let t: any
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(t)
|
||||
t = setTimeout(() => fn(...args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
const updateFilter = debounce((columnId: string, val: string) => {
|
||||
const newFilters = { ...filters.value }
|
||||
|
||||
if (val) newFilters[columnId] = val
|
||||
else delete newFilters[columnId]
|
||||
|
||||
filters.value = newFilters
|
||||
}, 400)
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?${params}`
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
})
|
||||
}
|
||||
|
||||
const selectedCount = ref({
|
||||
product_id: null,
|
||||
count: 0
|
||||
})
|
||||
|
||||
function getIcon(name: string) {
|
||||
if (sortField.value[0] === name) {
|
||||
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
||||
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
|
||||
}
|
||||
return 'i-lucide-arrow-up-down'
|
||||
}
|
||||
|
||||
const UInputNumber = resolveComponent('UInputNumber')
|
||||
const UInput = resolveComponent('UInput')
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
{
|
||||
id: 'expand',
|
||||
cell: ({ row }) =>
|
||||
h(UButton, {
|
||||
color: 'neutral',
|
||||
variant: 'ghost',
|
||||
icon: 'i-lucide-chevron-down',
|
||||
square: true,
|
||||
'aria-label': 'Expand',
|
||||
ui: {
|
||||
leadingIcon: [
|
||||
'transition-transform',
|
||||
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
|
||||
]
|
||||
},
|
||||
onClick: () => row.toggleExpanded()
|
||||
})
|
||||
},
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['product_id', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'ID'),
|
||||
h(UIcon, {
|
||||
name: getIcon('product_id')
|
||||
})
|
||||
]),
|
||||
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: filters.value[column.id] ?? '',
|
||||
'onUpdate:modelValue': (val: string) => {
|
||||
updateFilter(column.id, val)
|
||||
},
|
||||
size: 'xs'
|
||||
})
|
||||
])
|
||||
},
|
||||
// header: '#',
|
||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'image_link',
|
||||
header: 'Image',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image_link') as string,
|
||||
style: 'width:40px;height:40px;object-fit:cover;'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['name', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'Name'),
|
||||
h(UIcon, {
|
||||
name: getIcon('name')
|
||||
})
|
||||
]),
|
||||
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: filters.value[column.id] ?? '',
|
||||
'onUpdate:modelValue': (val: string) => {
|
||||
updateFilter(column.id, val)
|
||||
},
|
||||
size: 'xs'
|
||||
})
|
||||
])
|
||||
},
|
||||
cell: ({ row }) => row.getValue('name') as string,
|
||||
filterFn: (row, columnId, value) => {
|
||||
const name = row.getValue(columnId) as string
|
||||
return name.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['quantity', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'In stock'),
|
||||
h(UIcon, {
|
||||
name: getIcon('quantity')
|
||||
})
|
||||
]),
|
||||
])
|
||||
},
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: 'Count',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const columnsChild: TableColumn<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: '',
|
||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'image_link',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image_link') as string,
|
||||
style: 'width:40px;height:40px;object-fit:cover;'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('name') as string
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
fetchProductList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<div class="container mx-auto">
|
||||
<div class="flex flex-col gap-5 mb-6">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1>
|
||||
|
||||
|
||||
@@ -1,106 +1,114 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="container mx-auto mt-20">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex flex-col gap-5 mb-6">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1>
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
|
||||
<UForm @submit.prevent="saveAccount" :validate="validate" class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<UIcon name="mdi:domain"
|
||||
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Company Information') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('Company Name') }} *</label>
|
||||
<UInput v-model="formData.companyName" :placeholder="t('Enter company name')"
|
||||
name="companyName" class="w-full" />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('Company Email') }} *</label>
|
||||
<UInput v-model="formData.companyEmail" type="email"
|
||||
:placeholder="t('Enter company email')" name="companyEmail" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('REGON') }}</label>
|
||||
<UInput v-model="formData.regon" :placeholder="t('Enter REGON')" name="regon"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('NIP')
|
||||
}}</label>
|
||||
<UInput v-model="formData.nip" :placeholder="t('Enter NIP')" name="nip"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('VAT')
|
||||
}}</label>
|
||||
<UInput v-model="formData.vat" :placeholder="t('Enter VAT')" name="vat"
|
||||
class="w-full" />
|
||||
<div class="container mx-auto">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex flex-col gap-5 mb-6">
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1>
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
|
||||
<UForm @submit.prevent="saveAccount" :validate="validate" class="space-y-6">
|
||||
<div>
|
||||
<h2
|
||||
class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<UIcon name="mdi:domain"
|
||||
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Company Information') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('Company Name') }} *</label>
|
||||
<UInput v-model="formData.companyName" :placeholder="t('Enter company name')"
|
||||
name="companyName" class="w-full" />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('Company Email') }} *</label>
|
||||
<UInput v-model="formData.companyEmail" type="email"
|
||||
:placeholder="t('Enter company email')" name="companyEmail"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('REGON') }}</label>
|
||||
<UInput v-model="formData.regon" :placeholder="t('Enter REGON')" name="regon"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('NIP')
|
||||
}}</label>
|
||||
<UInput v-model="formData.nip" :placeholder="t('Enter NIP')" name="nip"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||
t('VAT')
|
||||
}}</label>
|
||||
<UInput v-model="formData.vat" :placeholder="t('Enter VAT')" name="vat"
|
||||
class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<UIcon name="mdi:map-marker"
|
||||
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Select Addresses') }}
|
||||
</h2>
|
||||
<div
|
||||
class="bg-(--second-light) dark:bg-(--main-dark)">
|
||||
<div class="mb-4">
|
||||
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||
class="w-full bg-white dark:bg-(--black) text-black dark:text-white" />
|
||||
</div>
|
||||
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||
:class="cartStore.selectedAddressId === address.id
|
||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
<div class="flex-1">
|
||||
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }},
|
||||
{{ address.city }}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="text-center py-6">
|
||||
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||
<RouterLink :to="{ name: 'addresses' }"
|
||||
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
{{ t('Add Address') }}
|
||||
</RouterLink>
|
||||
<div>
|
||||
<h2
|
||||
class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<UIcon name="mdi:map-marker"
|
||||
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
{{ t('Select Addresses') }}
|
||||
</h2>
|
||||
<div class="bg-(--second-light) dark:bg-(--main-dark)">
|
||||
<div class="mb-4">
|
||||
<UInput v-model="addressSearchQuery" type="text"
|
||||
:placeholder="t('Search address')"
|
||||
class="w-full bg-white dark:bg-(--black) text-black dark:text-white" />
|
||||
</div>
|
||||
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||
:class="cartStore.selectedAddressId === address.id
|
||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||
<div class="flex-1">
|
||||
<p class="text-black dark:text-white font-medium">{{ address.street }}
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode
|
||||
}},
|
||||
{{ address.city }}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="text-center py-6">
|
||||
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||
<RouterLink :to="{ name: 'addresses' }"
|
||||
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||
{{ t('Add Address') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<UButton variant="outline" color="neutral" @click="goBack"
|
||||
class="text-black dark:text-white">
|
||||
{{ t('Cancel') }}
|
||||
</UButton>
|
||||
<UButton type="submit" color="primary"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||
<UIcon name="mdi:content-save" />
|
||||
{{ t('Save') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<UButton variant="outline" color="neutral" @click="goBack"
|
||||
class="text-black dark:text-white">
|
||||
{{ t('Cancel') }}
|
||||
</UButton>
|
||||
<UButton type="submit" color="primary"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||
<UIcon name="mdi:content-save" />
|
||||
{{ t('Save') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -170,7 +178,7 @@ function saveAccount() {
|
||||
vat: formData.value.vat,
|
||||
companyAddressId: formData.value.companyAddressId,
|
||||
billingAddressId: formData.value.billingAddressId,
|
||||
companyAddress : ''
|
||||
companyAddress: ''
|
||||
})
|
||||
|
||||
router.push({ name: 'profile-details' })
|
||||
|
||||
9
bo/src/components/customer/PageStatistic.vue
Normal file
9
bo/src/components/customer/PageStatistic.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
Statistic page
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Default from '@/layouts/default.vue'
|
||||
</script>
|
||||
@@ -1,20 +1,57 @@
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getMenu } from '@/router/menu'
|
||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
let menu = await getMenu() as NavigationMenuItem[]
|
||||
|
||||
const openAll = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
let categoryId = ref(route.params.category_id)
|
||||
function findPath(tree: NavigationMenuItem[], id: number, path: Array<number> = []): Array<number> | null {
|
||||
for (let item of tree) {
|
||||
let newPath: Array<number> = [...path, item.category_id]
|
||||
if (item.category_id === id) {
|
||||
return newPath
|
||||
}
|
||||
if (item.children) {
|
||||
const result: Array<number> | null = findPath(item.children, id, newPath)
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
let path = findPath(menu, Number(categoryId.value))
|
||||
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
for (const item of menu) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
console.log(item);
|
||||
item.open = path && path.includes(item.category_id) ? true : openAll.value
|
||||
adaptMenu(item.children);
|
||||
item.open = openAll.value
|
||||
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
|
||||
item.children.unshift({
|
||||
label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: {
|
||||
name: 'customer-products-category', params: {
|
||||
category_id: item.params.category_id,
|
||||
link_rewrite: item.params.link_rewrite
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
item.to = { name: 'category', params: item.params };
|
||||
item.to = {
|
||||
name: 'customer-products-category', params: {
|
||||
category_id: item.params.category_id,
|
||||
link_rewrite: item.params.link_rewrite
|
||||
}
|
||||
};
|
||||
item.icon = 'i-lucide-file-text'
|
||||
}
|
||||
}
|
||||
@@ -22,15 +59,10 @@ function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
}
|
||||
|
||||
menu = adaptMenu(menu)
|
||||
|
||||
const items = ref<NavigationMenuItem[][]>([
|
||||
[
|
||||
...menu as NavigationMenuItem[]
|
||||
],
|
||||
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
|
||||
</template>
|
||||
</script>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { getMenu } from '@/router/menu'
|
||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
import { ref } from 'vue';
|
||||
let menu = await getMenu() as NavigationMenuItem[]
|
||||
|
||||
const openAll = ref(false)
|
||||
|
||||
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
for (const item of menu) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
adaptMenu(item.children);
|
||||
item.open = openAll.value
|
||||
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
|
||||
} else {
|
||||
item.to = { name: 'category', params: item.params };
|
||||
item.icon = 'i-lucide-file-text'
|
||||
}
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
menu = adaptMenu(menu)
|
||||
|
||||
const items = ref<NavigationMenuItem[][]>([
|
||||
[
|
||||
...menu as NavigationMenuItem[]
|
||||
],
|
||||
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
|
||||
</template>
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
{{ locale }}
|
||||
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||
valueKey="iso_code" :searchInput="false">
|
||||
<USelectMenu v-model="locale" :items="langs"
|
||||
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code"
|
||||
:searchInput="false">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span>
|
||||
<!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> -->
|
||||
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
||||
<span class="text-md ">{{ item.flag }}</span>
|
||||
<!-- <span class="text-md ">{{ item.flag }}</span> -->
|
||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,6 +23,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useCookie } from '@/composable/useCookie'
|
||||
import { computed, watch } from 'vue'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -50,9 +51,22 @@ const locale = computed({
|
||||
router.replace({ path: '/' + value + currentPath, query: route.query })
|
||||
}
|
||||
}
|
||||
|
||||
changeLang()
|
||||
},
|
||||
})
|
||||
|
||||
async function changeLang() {
|
||||
try {
|
||||
const { items } = await useFetchJson('/api/v1/public/auth/update-choice', {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.locale,
|
||||
(newLocale) => {
|
||||
|
||||
@@ -4,9 +4,11 @@ import TopBar from '@/components/TopBar.vue';
|
||||
|
||||
<template>
|
||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||
<main class="p-10">
|
||||
<TopBar/>
|
||||
<slot></slot>
|
||||
<main class="pt-20 pb-10">
|
||||
<TopBar />
|
||||
<div class="container mx-auto px-4">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +49,41 @@ const router = createRouter({
|
||||
],
|
||||
})
|
||||
|
||||
const viewModules = import.meta.glob('/src/views/**/*.vue')
|
||||
const componentModules = import.meta.glob('/src/components/**/*.vue')
|
||||
|
||||
async function setRoutes() {
|
||||
const routes = await getRoutes()
|
||||
|
||||
for (const item of routes) {
|
||||
const componentName = item.component
|
||||
const [, folder] = componentName.split('/')
|
||||
const componentPath = `/src${componentName}`
|
||||
|
||||
|
||||
let modules =
|
||||
folder === 'views' ? viewModules : componentModules
|
||||
|
||||
const importer = modules[componentPath]
|
||||
|
||||
if (!importer) {
|
||||
console.error('Component not found:', componentPath)
|
||||
continue
|
||||
}
|
||||
|
||||
const importedComponent = (await importer()).default
|
||||
|
||||
router.addRoute('locale', {
|
||||
path: item.path,
|
||||
component: importedComponent,
|
||||
name: item.name,
|
||||
meta: item.meta ? JSON.parse(item.meta) : {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await setRoutes()
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
const locale = to.params.locale as string
|
||||
const localeLang = langs.find((x) => x.iso_code === locale)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useFetchJson } from "@/composable/useFetchJson";
|
||||
import type { MenuItem, Route } from "@/types/menu";
|
||||
import { ref } from "vue";
|
||||
import { settings } from "./settings";
|
||||
|
||||
|
||||
const categoryId = ref()
|
||||
export const getMenu = async () => {
|
||||
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu');
|
||||
|
||||
if(!categoryId.value){
|
||||
categoryId.value = settings['app'].category_tree_root_id
|
||||
}
|
||||
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${categoryId.value}`);
|
||||
return resp.items.children
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const getRoutes = async () => {
|
||||
const resp = await useFetchJson<Route[]>('/api/v1/public/menu/get-routes');
|
||||
|
||||
return resp.items
|
||||
|
||||
}
|
||||
@@ -13,24 +13,24 @@ const products = [
|
||||
// type CategoryProducts = {}
|
||||
|
||||
export const useCategoryStore = defineStore('category', () => {
|
||||
const id_category = ref(0)
|
||||
const idCategory = ref(0)
|
||||
const categoryProducts = ref(products)
|
||||
|
||||
|
||||
function setCategoryID(id: number) {
|
||||
id_category.value = id
|
||||
idCategory.value = id
|
||||
}
|
||||
|
||||
async function getCategoryProducts() {
|
||||
return new Promise<typeof products>((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('Fetching products from category id: ', id_category.value);
|
||||
// console.log('Fetching products from category id: ', idCategory.value);
|
||||
resolve(categoryProducts.value)
|
||||
}, 2000 * Math.random())
|
||||
})
|
||||
}
|
||||
return {
|
||||
id_category,
|
||||
idCategory,
|
||||
getCategoryProducts,
|
||||
setCategoryID
|
||||
}
|
||||
|
||||
@@ -34,9 +34,8 @@ export const useProductStore = defineStore('product', () => {
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ProductDescription>(
|
||||
`/api/v1/restricted/product-description/get-product-description?productID=${productID}&productLangID=${langId}`
|
||||
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
|
||||
)
|
||||
console.log(response, 'dfsfsdf');
|
||||
productDescription.value = response.items
|
||||
|
||||
} catch (e: unknown) {
|
||||
|
||||
13
bo/src/types/menu.d.ts
vendored
13
bo/src/types/menu.d.ts
vendored
@@ -31,14 +31,15 @@ export interface TopMenuItem {
|
||||
|
||||
export interface Label {
|
||||
label: string
|
||||
trans:LabelTrans
|
||||
trans: LabelTrans
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface LabelTrans{
|
||||
pl:LabelItem
|
||||
en:LabelItem
|
||||
de: LabelItem
|
||||
}
|
||||
export interface LabelTrans {
|
||||
pl: LabelItem
|
||||
en: LabelItem
|
||||
de: LabelItem
|
||||
}
|
||||
export interface LabelItem {
|
||||
label: string
|
||||
}
|
||||
|
||||
10
bo/src/types/product.d.ts
vendored
10
bo/src/types/product.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
export interface ProductDescription {
|
||||
export interface ProductDescription {
|
||||
id?: number
|
||||
name?: string
|
||||
description: string
|
||||
@@ -6,4 +6,12 @@
|
||||
meta_description: string
|
||||
available_now: string
|
||||
usage: string
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
1
bo/src/types/settings.d.ts
vendored
1
bo/src/types/settings.d.ts
vendored
@@ -7,6 +7,7 @@ export interface Settings {
|
||||
}
|
||||
|
||||
export interface App {
|
||||
category_tree_root_id: number
|
||||
name: string
|
||||
environment: string
|
||||
base_url: string
|
||||
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/list-products/get-listing?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62"
|
||||
url: "{{bas_url}}/restricted/list/list-products?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62"
|
||||
params:
|
||||
- name: p
|
||||
value: "1"
|
||||
|
||||
@@ -10,12 +10,12 @@ http:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"q": "kinder",
|
||||
"q": "mat",
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
// "filter": "'attr.10'= 71",
|
||||
"facets":["category_ids", "price"]
|
||||
// "facets": ["category_ids", "attr", "feat", "price"]
|
||||
"filter": "'category_ids'= 10",
|
||||
// "facets":["category_ids", "price"]
|
||||
"facets": ["category_ids", "attr", "feat", "price"]
|
||||
}
|
||||
auth:
|
||||
type: bearer
|
||||
|
||||
28
bruno/b2b-daniel/save-product-description.yml
Normal file
28
bruno/b2b-daniel/save-product-description.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: save-product-description
|
||||
type: http
|
||||
seq: 19
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=3
|
||||
params:
|
||||
- name: productID
|
||||
value: "1"
|
||||
type: query
|
||||
- name: productLangID
|
||||
value: "3"
|
||||
type: query
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"description": "<p>Der Einsatz von Rehabilitationsrollen in verschiedenen Übungen und Behandlungen wirkt sich positiv auf die Reduzierung von Verletzungen und die Genesungschancen aus. Sie werden in der Rehabilitation, bei Korrekturgymnastik sowie in der traditionellen und Sportmassage eingesetzt, da sie ideal zum Anheben und Spreizen von Gliedmaßen geeignet sind. Zudem können sie zur Unterstützung von Knien, Füßen, Armen und Schultern verwendet werden. Auch für Kinder sind Rehabilitationsrollen empfehlenswert; ihre spielerische Anwendung fördert die Entwicklung der Grobmotorik.</p><p> Dank der großen Auswahl an Farben und Größen lässt sich ein Übungsset zusammenstellen, das in jeder Physiotherapiepraxis, jedem Massageraum, jeder Schule oder jedem Kindergarten benötigt wird.</p><p> Die Rehabilitationsrolle ist ein Medizinprodukt, das den grundlegenden Anforderungen an Medizinprodukte und den Bestimmungen des Medizinproduktegesetzes entspricht, im Register für Medizinprodukte des Amtes für die Registrierung von Arzneimitteln, Medizinprodukten und Biozidprodukten eingetragen ist, mit der Konformitätserklärung des Herstellers versehen ist und das CE-Zeichen trägt. </p><p></p><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/images.jpg\" alt=\"Medizinprodukt\" style=\"margin-left:auto;margin-right:auto;\" width=\"253\" height=\"86\" /></p><h4> <strong>Empfohlene Verwendung:</strong></h4><ul style=\"list-style-type:circle;\"><li> in der Rehabilitation</li><li> während Massagen (traditionell, Sport)</li><li> in der Korrekturgymnastik (insbesondere für Kinder)</li><li> zur Linderung von Verletzungen einzelner Körperteile</li><li> Zur Unterstützung von: Knien, Knöcheln, Kopf des Patienten</li><li> bei Übungen zur Entwicklung der motorischen Fähigkeiten von Kindern</li><li> in Schönheitssalons</li><li> in Kinderspielzimmern</li></ul><p></p><h4> <strong>Materialspezifikationen:</strong></h4><p> <strong>Abdeckung:</strong> PVC-beschichtetes Material, das für medizinische Geräte vorgesehen ist und daher sehr leicht zu reinigen und zu desinfizieren ist:</p><ul style=\"list-style-type:circle;\"><li> Material gemäß REACH-Verordnung, zertifiziert mit dem STANDARD 100 Zertifikat von OEKO-TEX®.</li><li> Enthält keine Phthalate</li><li> feuerfest</li><li> resistent gegenüber physiologischen Flüssigkeiten (Blut, Urin, Schweiß) und Alkohol</li><li> UV-beständig, daher auch für den Einsatz im Freien geeignet.</li><li> kratzfest</li><li> ölbeständig </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/reach.jpg\" alt=\"ERREICHEN\" width=\"115\" height=\"115\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"116\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/phthalate-free.jpg\" alt=\"Enthält keine Phthalate\" width=\"112\" height=\"111\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/fireresistant.jpg\" alt=\"Feuerfest\" width=\"114\" height=\"113\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-alkohol.jpg\" alt=\"Alkoholbeständig\" width=\"114\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-uv.jpg\" alt=\"UV-beständig\" width=\"117\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/outdoor.jpg\" alt=\"Für den Einsatz im Freien konzipiert\" width=\"116\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-zadrapania.jpg\" alt=\"Kratzfest\" width=\"97\" height=\"96\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/olejoodporny.jpg\" alt=\"Ölbeständig\" width=\"99\" height=\"98\" /></p><p> <strong>Füllung:</strong> mittelharter Polyurethanschaum mit erhöhter Verformungsbeständigkeit:</p><ul style=\"list-style-type:circle;\"><li> besitzt ein Hygienezertifikat, ausgestellt vom Institut für Maritime und Tropenmedizin in Gdynia</li><li> zertifiziert mit dem STANDARD 100 by OEKO-TEX® Zertifikat – Produktklasse I, ausgestellt vom Textilforschungsinstitut in Łódź</li><li> Hergestellt aus hochwertigen Rohstoffen, die die Ozonschicht nicht schädigen. </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"95\" height=\"95\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/Logo_GUMed_kolor-180x180.jpg\" alt=\"Hygienezertifikat\" width=\"94\" height=\"94\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/atest_higieniczny_kolor.jpg\" alt=\"Hygienezertifikat\" width=\"79\" height=\"94\" /></p><p></p><p></p>"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
28
bruno/b2b-daniel/translate-product-description.yml
Normal file
28
bruno/b2b-daniel/translate-product-description.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: translate-product-description
|
||||
type: http
|
||||
seq: 21
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=1&productToLangID=3&model=Google
|
||||
params:
|
||||
- name: productID
|
||||
value: "51"
|
||||
type: query
|
||||
- name: productFromLangID
|
||||
value: "1"
|
||||
type: query
|
||||
- name: productToLangID
|
||||
value: "3"
|
||||
type: query
|
||||
- name: model
|
||||
value: Google
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
9
bruno/b2b_daniel/.gitignore
vendored
Normal file
9
bruno/b2b_daniel/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Secrets
|
||||
.env*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
7
bruno/b2b_daniel/auth/folder.yml
Normal file
7
bruno/b2b_daniel/auth/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: auth
|
||||
type: folder
|
||||
seq: 1
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
22
bruno/b2b_daniel/auth/update-choice.yml
Normal file
22
bruno/b2b_daniel/auth/update-choice.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
name: update-choice
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=0&country_id=1
|
||||
params:
|
||||
- name: lang_id
|
||||
value: "0"
|
||||
type: query
|
||||
- name: country_id
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
15
bruno/b2b_daniel/carts/add-new-cart.yml
Normal file
15
bruno/b2b_daniel/carts/add-new-cart.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: add-new-cart
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/add-new-cart
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
25
bruno/b2b_daniel/carts/add-product-to-cart (1).yml
Normal file
25
bruno/b2b_daniel/carts/add-product-to-cart (1).yml
Normal file
@@ -0,0 +1,25 @@
|
||||
info:
|
||||
name: add-product-to-cart (1)
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&amount=1
|
||||
params:
|
||||
- name: cart_id
|
||||
value: "1"
|
||||
type: query
|
||||
- name: product_id
|
||||
value: "51"
|
||||
type: query
|
||||
- name: amount
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
28
bruno/b2b_daniel/carts/add-product-to-cart.yml
Normal file
28
bruno/b2b_daniel/carts/add-product-to-cart.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: add-product-to-cart
|
||||
type: http
|
||||
seq: 14
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=1
|
||||
params:
|
||||
- name: cart_id
|
||||
value: "1"
|
||||
type: query
|
||||
- name: product_id
|
||||
value: "51"
|
||||
type: query
|
||||
- name: product_attribute_id
|
||||
value: "1115"
|
||||
type: query
|
||||
- name: amount
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
22
bruno/b2b_daniel/carts/change-cart-name.yml
Normal file
22
bruno/b2b_daniel/carts/change-cart-name.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
name: change-cart-name
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/change-cart-name?cart_id=1&new_name=test
|
||||
params:
|
||||
- name: cart_id
|
||||
value: "1"
|
||||
type: query
|
||||
- name: new_name
|
||||
value: test
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/b2b_daniel/carts/folder.yml
Normal file
7
bruno/b2b_daniel/carts/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: carts
|
||||
type: folder
|
||||
seq: 7
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
19
bruno/b2b_daniel/carts/retrieve-cart.yml
Normal file
19
bruno/b2b_daniel/carts/retrieve-cart.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: retrieve-cart
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=3
|
||||
params:
|
||||
- name: cart_id
|
||||
value: "3"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
15
bruno/b2b_daniel/carts/retrieve-carts-info.yml
Normal file
15
bruno/b2b_daniel/carts/retrieve-carts-info.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: retrieve-carts-info
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/carts/retrieve-carts-info
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/b2b_daniel/langs-and-countries/folder.yml
Normal file
7
bruno/b2b_daniel/langs-and-countries/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: langs-and-countries
|
||||
type: folder
|
||||
seq: 4
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
15
bruno/b2b_daniel/langs-and-countries/get_countries.yml
Normal file
15
bruno/b2b_daniel/langs-and-countries/get_countries.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: get_countries
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/langs-and-countries/get-countries
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/b2b_daniel/list/folder.yml
Normal file
7
bruno/b2b_daniel/list/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: list
|
||||
type: folder
|
||||
seq: 3
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
24
bruno/b2b_daniel/list/list-products.yml
Normal file
24
bruno/b2b_daniel/list/list-products.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
info:
|
||||
name: list-products
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/list/list-products?p=1&elems=10&target_user_id=2
|
||||
params:
|
||||
- name: p
|
||||
value: "1"
|
||||
type: query
|
||||
- name: elems
|
||||
value: "10"
|
||||
type: query
|
||||
- name: target_user_id
|
||||
value: "2"
|
||||
type: query
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
21
bruno/b2b_daniel/list/list-users.yml
Normal file
21
bruno/b2b_daniel/list/list-users.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
info:
|
||||
name: list-users
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/list/list-users?p=1&elems=10
|
||||
params:
|
||||
- name: p
|
||||
value: "1"
|
||||
type: query
|
||||
- name: elems
|
||||
value: "10"
|
||||
type: query
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/b2b_daniel/menu/folder.yml
Normal file
7
bruno/b2b_daniel/menu/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: menu
|
||||
type: folder
|
||||
seq: 5
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
22
bruno/b2b_daniel/menu/get-breadcrumb.yml
Normal file
22
bruno/b2b_daniel/menu/get-breadcrumb.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
name: get-breadcrumb
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13
|
||||
params:
|
||||
- name: root_category_id
|
||||
value: "10"
|
||||
type: query
|
||||
- name: category_id
|
||||
value: "13"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
19
bruno/b2b_daniel/menu/get-category-tree.yml
Normal file
19
bruno/b2b_daniel/menu/get-category-tree.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: get-category-tree
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=10
|
||||
params:
|
||||
- name: root_category_id
|
||||
value: "10"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
21
bruno/b2b_daniel/opencollection.yml
Normal file
21
bruno/b2b_daniel/opencollection.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: b2b-daniel
|
||||
config:
|
||||
proxy:
|
||||
inherit: true
|
||||
config:
|
||||
protocol: http
|
||||
hostname: ""
|
||||
port: ""
|
||||
auth:
|
||||
username: ""
|
||||
password: ""
|
||||
bypassProxy: ""
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
7
bruno/b2b_daniel/product-translation/folder.yml
Normal file
7
bruno/b2b_daniel/product-translation/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: product-translation
|
||||
type: folder
|
||||
seq: 2
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
name: get-product-description
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=1
|
||||
params:
|
||||
- name: productID
|
||||
value: "51"
|
||||
type: query
|
||||
- name: productLangID
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: translate-product-description
|
||||
type: http
|
||||
seq: 24
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=2&productToLangID=3&model=Google
|
||||
params:
|
||||
- name: productID
|
||||
value: "51"
|
||||
type: query
|
||||
- name: productFromLangID
|
||||
value: "2"
|
||||
type: query
|
||||
- name: productToLangID
|
||||
value: "3"
|
||||
type: query
|
||||
- name: model
|
||||
value: Google
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
15
bruno/b2b_daniel/search/create-index.yml
Normal file
15
bruno/b2b_daniel/search/create-index.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: create-index
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:3000/api/v1/restricted/search/create-index
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/b2b_daniel/search/folder.yml
Normal file
7
bruno/b2b_daniel/search/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: search
|
||||
type: folder
|
||||
seq: 6
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
17
bruno/b2b_daniel/search/get-indexes.yml
Normal file
17
bruno/b2b_daniel/search/get-indexes.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
info:
|
||||
name: get-indexes
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:7700/indexes
|
||||
auth:
|
||||
type: bearer
|
||||
token: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
17
bruno/b2b_daniel/search/remove-index.yml
Normal file
17
bruno/b2b_daniel/search/remove-index.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
info:
|
||||
name: remove-index
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
url: http://localhost:7700/indexes/meili_products_shop1_lang1
|
||||
auth:
|
||||
type: bearer
|
||||
token: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user