Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate
This commit is contained in:
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "./app/cmd/main.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func AuthMiddleware() fiber.Handler {
|
||||
// Set user in context
|
||||
c.Locals(constdata.USER_LOCALES_NAME, user.ToSession())
|
||||
c.Locals(constdata.USER_LOCALES_ID, user.ID)
|
||||
|
||||
c.Locals(constdata.LANG_LOCALES_ID, user.LangID)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func RequireAdmin() fiber.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
if userSession.Role != model.RoleAdmin {
|
||||
if model.CustomerRole(userSession.RoleName) != model.RoleAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "admin access required",
|
||||
})
|
||||
|
||||
28
app/delivery/middleware/permissions.go
Normal file
28
app/delivery/middleware/permissions.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Require(p perms.Permission) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
u := c.Locals("user")
|
||||
if u == nil {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
user, ok := u.(*model.UserSession)
|
||||
if !ok {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, perm := range user.Permissions {
|
||||
if perm == p {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
return c.SendStatus(fiber.StatusForbidden)
|
||||
}
|
||||
}
|
||||
11
app/delivery/middleware/perms/permissions.go
Normal file
11
app/delivery/middleware/perms/permissions.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package perms
|
||||
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
UserRead Permission = "user.read"
|
||||
UserWrite Permission = "user.write"
|
||||
UserReadAny Permission = "user.read.any"
|
||||
UserWriteAny Permission = "user.write.any"
|
||||
UserDeleteAny Permission = "user.delete.any"
|
||||
)
|
||||
@@ -360,7 +360,7 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
|
||||
user := model.Customer{
|
||||
ID: userLocals.UserID,
|
||||
Email: userLocals.Email,
|
||||
Role: userLocals.Role,
|
||||
Role: model.Role{ID: userLocals.RoleID, Name: userLocals.RoleName},
|
||||
LangID: userLocals.LangID,
|
||||
CountryID: userLocals.CountryID,
|
||||
IsActive: userLocals.IsActive,
|
||||
|
||||
68
app/delivery/web/api/restricted/currency.go
Normal file
68
app/delivery/web/api/restricted/currency.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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/currencyService"
|
||||
"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 CurrencyHandler struct {
|
||||
CurrencyService *currencyService.CurrencyService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewCurrencyHandler() *CurrencyHandler {
|
||||
currencyService := currencyService.New()
|
||||
return &CurrencyHandler{
|
||||
CurrencyService: currencyService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func CurrencyHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewCurrencyHandler()
|
||||
|
||||
r.Post("/currency-rate", handler.PostCurrencyRate)
|
||||
r.Get("/currency-rate/:id", handler.GetCurrencyRate)
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
|
||||
var currencyRate model.CurrencyRate
|
||||
if err := c.Bind().Body(¤cyRate); err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrJSONBody)))
|
||||
}
|
||||
|
||||
err := h.CurrencyService.CreateCurrencyRate(¤cyRate)
|
||||
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(""), 1, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
|
||||
func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
}
|
||||
|
||||
currency, err := h.CurrencyService.GetCurrency(uint(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(currency, 0, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
70
app/delivery/web/api/restricted/customer.go
Normal file
70
app/delivery/web/api/restricted/customer.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/customerService"
|
||||
"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 customerHandler struct {
|
||||
service *customerService.CustomerService
|
||||
}
|
||||
|
||||
func NewCustomerHandler() *customerHandler {
|
||||
customerService := customerService.New()
|
||||
return &customerHandler{
|
||||
service: customerService,
|
||||
}
|
||||
}
|
||||
|
||||
func CustomerHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewCustomerHandler()
|
||||
|
||||
r.Get("", handler.customerData)
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *customerHandler) customerData(fc fiber.Ctx) error {
|
||||
var customerId uint
|
||||
customerIdStr := fc.Query("id")
|
||||
if customerIdStr != "" {
|
||||
user, ok := fc.Locals("user").(*model.UserSession)
|
||||
if !ok {
|
||||
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
id, err := strconv.ParseUint(customerIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) {
|
||||
return fc.Status(fiber.StatusForbidden).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
|
||||
}
|
||||
|
||||
customerId = uint(id)
|
||||
} else {
|
||||
id, ok := fc.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
customerId = id
|
||||
}
|
||||
|
||||
customer, err := h.service.GetById(customerId)
|
||||
if err != nil {
|
||||
return fc.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
|
||||
}
|
||||
|
||||
return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK)))
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package restricted
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||
@@ -86,12 +88,12 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
|
||||
lang_id, ok := c.Locals("langID").(uint)
|
||||
session, ok := c.Locals("user").(*model.UserSession)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
menu, err := h.menuService.GetTopMenu(lang_id)
|
||||
menu, err := h.menuService.GetTopMenu(session.LangID, session.RoleID)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
|
||||
82
app/delivery/web/api/restricted/product.go
Normal file
82
app/delivery/web/api/restricted/product.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/service/productService"
|
||||
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"
|
||||
)
|
||||
|
||||
type ProductsHandler struct {
|
||||
productService *productService.ProductService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewListProductsHandler creates a new ListProductsHandler instance
|
||||
func NewProductsHandler() *ProductsHandler {
|
||||
productService := productService.New()
|
||||
return &ProductsHandler{
|
||||
productService: productService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewProductsHandler()
|
||||
|
||||
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
|
||||
p_id_product, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
country_idStr := c.Params("country_id")
|
||||
|
||||
b2b_id_country, err := strconv.Atoi(country_idStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
quantityStr := c.Params("quantity")
|
||||
|
||||
p_quantity, err := strconv.Atoi(quantityStr)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
p_id_customer, ok := c.Locals(constdata.USER_LOCALES_ID).(uint)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
fmt.Printf("p_id_customer: %v\n", p_id_customer)
|
||||
id_lang, ok := c.Locals(constdata.LANG_LOCALES_ID).(uint)
|
||||
if !ok {
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||
}
|
||||
productJson, err := h.productService.GetJSON(p_id_product, int(id_lang), int(p_id_customer), b2b_id_country, p_quantity)
|
||||
if err != nil {
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(&productJson, 1, i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
@@ -90,6 +90,9 @@ func (s *Server) Setup() error {
|
||||
menuRouting := s.public.Group("/menu")
|
||||
public.RoutingHandlerRoutes(menuRouting)
|
||||
|
||||
pCustomer := s.restricted.Group("/customer")
|
||||
restricted.CustomerHandlerRoutes(pCustomer)
|
||||
|
||||
// product translation routes (restricted)
|
||||
productTranslation := s.restricted.Group("/product-translation")
|
||||
restricted.ProductTranslationHandlerRoutes(productTranslation)
|
||||
@@ -98,6 +101,9 @@ func (s *Server) Setup() error {
|
||||
list := s.restricted.Group("/list")
|
||||
restricted.ListHandlerRoutes(list)
|
||||
|
||||
product := s.restricted.Group("/product")
|
||||
restricted.ProductsHandlerRoutes(product)
|
||||
|
||||
// locale selector (restricted)
|
||||
// this is basically for changing user's selected language and country
|
||||
localeSelector := s.restricted.Group("/langs-and-countries")
|
||||
@@ -119,6 +125,8 @@ func (s *Server) Setup() error {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
})
|
||||
|
||||
restricted.CurrencyHandlerRoutes(s.restricted)
|
||||
|
||||
// // Restricted routes example
|
||||
// restricted := s.api.Group("/restricted")
|
||||
// restricted.Use(middleware.AuthMiddleware())
|
||||
|
||||
25
app/model/currency.go
Normal file
25
app/model/currency.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Currency struct {
|
||||
ID int `json:"id"`
|
||||
PsIDCurrency uint `json:"ps_id_currency"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ConversionRate *float64 `json:"conversion_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (Currency) TableName() string {
|
||||
return "b2b_currencies"
|
||||
}
|
||||
|
||||
type CurrencyRate struct {
|
||||
B2bIdCurrency uint `json:"b2b_id_currency"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ConversionRate *float64 `json:"conversion_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (CurrencyRate) TableName() string {
|
||||
return "b2b_currency_rates"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,8 @@ type Customer struct {
|
||||
Password string `gorm:"size:255" json:"-"` // Hashed password, not exposed in JSON
|
||||
FirstName string `gorm:"size:100" json:"first_name"`
|
||||
LastName string `gorm:"size:100" json:"last_name"`
|
||||
Role CustomerRole `gorm:"type:varchar(20);default:'user'" json:"role"`
|
||||
RoleID uint `gorm:"column:role_id;not null;default:1" json:"-"`
|
||||
Role Role `gorm:"foreignKey:RoleID" json:"role"`
|
||||
Provider AuthProvider `gorm:"type:varchar(20);default:'local'" json:"provider"`
|
||||
ProviderID string `gorm:"size:255" json:"provider_id,omitempty"` // ID from OAuth provider
|
||||
AvatarURL string `gorm:"size:500" json:"avatar_url,omitempty"`
|
||||
@@ -32,14 +34,6 @@ type Customer struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// CustomerRole represents the role of a user
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
RoleUser CustomerRole = "user"
|
||||
RoleAdmin CustomerRole = "admin"
|
||||
)
|
||||
|
||||
// AuthProvider represents the authentication provider
|
||||
type AuthProvider string
|
||||
|
||||
@@ -53,16 +47,6 @@ func (Customer) TableName() string {
|
||||
return "b2b_customers"
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user has admin role
|
||||
func (u *Customer) IsAdmin() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// CanManageUsers checks if the user can manage other users
|
||||
func (u *Customer) CanManageUsers() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// FullName returns the user's full name
|
||||
func (u *Customer) FullName() string {
|
||||
if u.FirstName == "" && u.LastName == "" {
|
||||
@@ -73,27 +57,51 @@ func (u *Customer) FullName() string {
|
||||
|
||||
// UserSession represents a user session for JWT claims
|
||||
type UserSession struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role CustomerRole `json:"role"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
RoleID uint `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Permissions []perms.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
func (us *UserSession) HasPermission(permission perms.Permission) bool {
|
||||
for _, p := range us.Permissions {
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ToSession converts User to UserSession
|
||||
func (u *Customer) ToSession() *UserSession {
|
||||
|
||||
return &UserSession{
|
||||
UserID: u.ID,
|
||||
Email: u.Email,
|
||||
Role: u.Role,
|
||||
LangID: u.LangID,
|
||||
CountryID: u.CountryID,
|
||||
IsActive: u.IsActive,
|
||||
UserID: u.ID,
|
||||
Email: u.Email,
|
||||
RoleID: u.Role.ID,
|
||||
RoleName: u.Role.Name,
|
||||
Permissions: BuildPermissionSlice(u),
|
||||
LangID: u.LangID,
|
||||
CountryID: u.CountryID,
|
||||
IsActive: u.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildPermissionSlice(user *Customer) []perms.Permission {
|
||||
var perms []perms.Permission
|
||||
|
||||
for _, p := range user.Role.Permissions {
|
||||
perms = append(perms, p.Name)
|
||||
}
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
// LoginRequest represents the login form data
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" form:"email"`
|
||||
|
||||
18
app/model/model.go
Normal file
18
app/model/model.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement" swaggerignore:"true" json:"id,omitempty" hidden:"true"`
|
||||
CreatedAt time.Time `gorm:"not null;autoCreateTime" swaggerignore:"true" json:"-"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" swaggerignore:"true" json:"-"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true" json:"-"`
|
||||
}
|
||||
|
||||
// Makes all objects embedding db.Model implementators of ModelWithID interface
|
||||
func (m Model) ModelWithID() {
|
||||
}
|
||||
12
app/model/permission.go
Normal file
12
app/model/permission.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
||||
|
||||
type Permission struct {
|
||||
ID uint
|
||||
Name perms.Permission
|
||||
}
|
||||
|
||||
func (Permission) TableName() string {
|
||||
return "b2b_permissions"
|
||||
}
|
||||
19
app/model/role.go
Normal file
19
app/model/role.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
type Role struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:64" json:"name"`
|
||||
Permissions []Permission `gorm:"many2many:b2b_role_permissions;" json:"-"`
|
||||
}
|
||||
|
||||
func (Role) TableName() string {
|
||||
return "b2b_roles"
|
||||
}
|
||||
|
||||
type CustomerRole string
|
||||
|
||||
const (
|
||||
RoleUser CustomerRole = "user"
|
||||
RoleAdmin CustomerRole = "admin"
|
||||
RoleSuperAdmin CustomerRole = "super_admin"
|
||||
)
|
||||
53
app/repos/currencyRepo/currencyRepo.go
Normal file
53
app/repos/currencyRepo/currencyRepo.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package currencyRepo
|
||||
|
||||
import (
|
||||
"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/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type UICurrencyRepo interface {
|
||||
CreateConversionRate(currencyRate *model.CurrencyRate) error
|
||||
Get(id uint) (*model.Currency, error)
|
||||
}
|
||||
|
||||
type CurrencyRepo struct{}
|
||||
|
||||
func New() UICurrencyRepo {
|
||||
return &CurrencyRepo{}
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error {
|
||||
return db.DB.Create(currencyRate).Error
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
|
||||
var currency model.Currency
|
||||
|
||||
err := db.DB.Table("b2b_currencies c").
|
||||
Select("c.*, r.conversion_rate").
|
||||
Joins(`
|
||||
LEFT JOIN b2b_currency_rates r
|
||||
ON r.b2b_id_currency = c.id
|
||||
AND r.created_at = (
|
||||
SELECT MAX(created_at)
|
||||
FROM b2b_currency_rates
|
||||
WHERE b2b_id_currency = c.id
|
||||
)
|
||||
`).
|
||||
Where("c.id = ?", id).
|
||||
Scan(¤cy).Error
|
||||
|
||||
return ¤cy, err
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
|
||||
|
||||
found, err := find.Paginate[model.Currency](langId, p, db.DB.
|
||||
Model(&model.Currency{}).
|
||||
Scopes(filt.All()...),
|
||||
)
|
||||
|
||||
return &found, err
|
||||
}
|
||||
27
app/repos/customerRepo/customerRepo.go
Normal file
27
app/repos/customerRepo/customerRepo.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package customerRepo
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
)
|
||||
|
||||
type UICustomerRepo interface {
|
||||
Get(id uint) (*model.Customer, error)
|
||||
}
|
||||
|
||||
type CustomerRepo struct{}
|
||||
|
||||
func New() UICustomerRepo {
|
||||
return &CustomerRepo{}
|
||||
}
|
||||
|
||||
func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) {
|
||||
var customer model.Customer
|
||||
|
||||
err := db.DB.
|
||||
Preload("Role").
|
||||
First(&customer, id).
|
||||
Error
|
||||
|
||||
return &customer, err
|
||||
}
|
||||
39
app/repos/productsRepo/productsRepo.go
Normal file
39
app/repos/productsRepo/productsRepo.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package productsRepo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||
)
|
||||
|
||||
type UIProductsRepo interface {
|
||||
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
|
||||
}
|
||||
|
||||
type ProductsRepo struct{}
|
||||
|
||||
func New() UIProductsRepo {
|
||||
return &ProductsRepo{}
|
||||
}
|
||||
|
||||
func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
|
||||
var productStr string // ← Scan as string first
|
||||
|
||||
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`,
|
||||
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity).
|
||||
Scan(&productStr).
|
||||
Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optional: validate it's valid JSON
|
||||
if !json.Valid([]byte(productStr)) {
|
||||
return nil, fmt.Errorf("invalid json returned from stored procedure")
|
||||
}
|
||||
|
||||
raw := json.RawMessage(productStr)
|
||||
return &raw, nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type UIRoutesRepo interface {
|
||||
GetRoutes(langId uint) ([]model.Route, error)
|
||||
GetTopMenu(id uint) ([]model.B2BTopMenu, error)
|
||||
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
|
||||
}
|
||||
|
||||
type RoutesRepo struct{}
|
||||
@@ -26,12 +26,16 @@ func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func (p *RoutesRepo) GetTopMenu(id uint) ([]model.B2BTopMenu, error) {
|
||||
func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) {
|
||||
var menus []model.B2BTopMenu
|
||||
|
||||
err := db.Get().
|
||||
Where("active = ?", 1).
|
||||
Order("parent_id ASC, position ASC").
|
||||
err := db.
|
||||
Get().
|
||||
Model(model.B2BTopMenu{}).
|
||||
Joins("JOIN b2b_top_menu_roles tmr ON tmr.top_menu_id = b2b_top_menu.menu_id").
|
||||
Where(model.B2BTopMenu{Active: 1}).
|
||||
Where("tmr.role_id = ?", roleId).
|
||||
Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC").
|
||||
Find(&menus).Error
|
||||
|
||||
return menus, err
|
||||
|
||||
@@ -23,13 +23,13 @@ import (
|
||||
|
||||
// JWTClaims represents the JWT claims
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role model.CustomerRole `json:"customer_role"`
|
||||
CartsIDs []uint `json:"carts_ids"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"customer_role"`
|
||||
CartsIDs []uint `json:"carts_ids"`
|
||||
LangID uint `json:"lang_id"`
|
||||
CountryID uint `json:"country_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
|
||||
var user model.Customer
|
||||
|
||||
// Find user by email
|
||||
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", responseErrors.ErrInvalidCredentials
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
||||
Password: string(hashedPassword),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Role: model.RoleUser,
|
||||
Role: model.Role{},
|
||||
Provider: model.ProviderLocal,
|
||||
IsActive: false,
|
||||
EmailVerified: false,
|
||||
@@ -422,7 +422,7 @@ func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
|
||||
// GetUserByID retrieves a user by ID
|
||||
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
|
||||
var user model.Customer
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
if err := s.db.Preload("Role.Permissions").First(&user, userID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, responseErrors.ErrUserNotFound
|
||||
}
|
||||
@@ -489,7 +489,7 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Username: user.Email,
|
||||
Role: user.Role,
|
||||
Role: user.Role.Name,
|
||||
CartsIDs: []uint{},
|
||||
LangID: user.LangID,
|
||||
CountryID: user.CountryID,
|
||||
|
||||
@@ -150,7 +150,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
|
||||
Provider: model.ProviderGoogle,
|
||||
ProviderID: info.ID,
|
||||
AvatarURL: info.Picture,
|
||||
Role: model.RoleUser,
|
||||
Role: model.Role{},
|
||||
IsActive: true,
|
||||
EmailVerified: true,
|
||||
LangID: 2, // default is english
|
||||
|
||||
25
app/service/currencyService/currencyService.go
Normal file
25
app/service/currencyService/currencyService.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package currencyService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo"
|
||||
)
|
||||
|
||||
type CurrencyService struct {
|
||||
repo currencyRepo.UICurrencyRepo
|
||||
}
|
||||
|
||||
func (s *CurrencyService) GetCurrency(id uint) (*model.Currency, error) {
|
||||
return s.repo.Get(id)
|
||||
}
|
||||
|
||||
func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error {
|
||||
return s.repo.CreateConversionRate(currency)
|
||||
}
|
||||
|
||||
func New() *CurrencyService {
|
||||
repo := currencyRepo.New()
|
||||
return &CurrencyService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
20
app/service/customerService/customerService.go
Normal file
20
app/service/customerService/customerService.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package customerService
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/customerRepo"
|
||||
)
|
||||
|
||||
type CustomerService struct {
|
||||
repo customerRepo.UICustomerRepo
|
||||
}
|
||||
|
||||
func New() *CustomerService {
|
||||
return &CustomerService{
|
||||
repo: customerRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CustomerService) GetById(id uint) (*model.Customer, error) {
|
||||
return s.repo.Get(id)
|
||||
}
|
||||
@@ -176,8 +176,8 @@ func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uin
|
||||
return breadcrumb, nil
|
||||
}
|
||||
|
||||
func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) {
|
||||
items, err := s.routesRepo.GetTopMenu(id)
|
||||
func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopMenu, error) {
|
||||
items, err := s.routesRepo.GetTopMenu(languageId, roleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
27
app/service/productService/productService.go
Normal file
27
app/service/productService/productService.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package productService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
|
||||
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||
)
|
||||
|
||||
type ProductService struct {
|
||||
productsRepo productsRepo.UIProductsRepo
|
||||
}
|
||||
|
||||
func New() *ProductService {
|
||||
return &ProductService{
|
||||
productsRepo: productsRepo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
|
||||
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
|
||||
if err != nil {
|
||||
return products, err
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
@@ -13,3 +13,4 @@ const DEFAULT_NEW_CART_NAME = "new cart"
|
||||
|
||||
const USER_LOCALES_NAME = "user"
|
||||
const USER_LOCALES_ID = "userID"
|
||||
const LANG_LOCALES_ID = "langID"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
var (
|
||||
// Typed errors for request validation and authentication
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrInvalidBody = errors.New("invalid request body")
|
||||
ErrNotAuthenticated = errors.New("not authenticated")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
@@ -59,6 +60,9 @@ var (
|
||||
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 data parsing
|
||||
ErrJSONBody = errors.New("invalid JSON body")
|
||||
)
|
||||
|
||||
// Error represents an error with HTTP status code
|
||||
@@ -83,6 +87,8 @@ func NewError(err error, status int) *Error {
|
||||
// GetErrorCode returns the error code string for HTTP response mapping
|
||||
func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
switch {
|
||||
case errors.Is(err, ErrForbidden):
|
||||
return i18n.T_(c, "error.err_forbidden")
|
||||
case errors.Is(err, ErrInvalidBody):
|
||||
return i18n.T_(c, "error.err_invalid_body")
|
||||
case errors.Is(err, ErrInvalidCredentials):
|
||||
@@ -162,6 +168,9 @@ 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, ErrJSONBody):
|
||||
return i18n.T_(c, "error.err_json_body")
|
||||
|
||||
default:
|
||||
return i18n.T_(c, "error.err_internal_server_error")
|
||||
}
|
||||
@@ -170,6 +179,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
||||
// GetErrorStatus returns the HTTP status code for the given error
|
||||
func GetErrorStatus(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, ErrForbidden):
|
||||
return fiber.StatusForbidden
|
||||
case errors.Is(err, ErrInvalidCredentials),
|
||||
errors.Is(err, ErrNotAuthenticated),
|
||||
errors.Is(err, ErrInvalidToken),
|
||||
@@ -203,7 +214,8 @@ func GetErrorStatus(err error) int {
|
||||
errors.Is(err, ErrRootNeverReached),
|
||||
errors.Is(err, ErrMaxAmtOfCartsReached),
|
||||
errors.Is(err, ErrUserHasNoSuchCart),
|
||||
errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||
errors.Is(err, ErrProductOrItsVariationDoesNotExist),
|
||||
errors.Is(err, ErrJSONBody):
|
||||
return fiber.StatusBadRequest
|
||||
case errors.Is(err, ErrEmailExists):
|
||||
return fiber.StatusConflict
|
||||
|
||||
1
bo/components.d.ts
vendored
1
bo/components.d.ts
vendored
@@ -33,6 +33,7 @@ declare module 'vue' {
|
||||
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']
|
||||
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
526
bo/src/components/admin/ProductDetailView copy.vue
Normal file
526
bo/src/components/admin/ProductDetailView copy.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<UIcon name="line-md:arrow-left" class="text-(--text-sky-light) dark:text-(--text-sky-dark)" />
|
||||
<p class="cursor-pointer text-(--text-sky-light) dark:text-(--text-sky-dark)" @click="backFromProduct()">
|
||||
Back to products</p>
|
||||
</div>
|
||||
<div class="container mx-auto ">
|
||||
<div
|
||||
class="gap-4 mb-6 bg-slate-50 dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
|
||||
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
|
||||
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
|
||||
<template #default>
|
||||
<div class="flex flex-col items-start leading-tight">
|
||||
<span class="text-xs text-gray-400">
|
||||
Selected language
|
||||
</span>
|
||||
<span class="font-medium dark:text-white text-black">
|
||||
{{langs.find(l => l.id === toLangId)?.name || 'Select language'}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item-leading="{ item }">
|
||||
<div class="flex items-center gap-2 cursor-pointer">
|
||||
<span class="text-lg">{{ item.flag }}</span>
|
||||
<span class="font-medium dark:text-white text-black">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations" class="flex gap-7">
|
||||
<UButton @click="translateToSelectedLanguage" color="info" :loading="translating">
|
||||
Translate from Polish to {{langs.find(l => l.id === toLangId)?.name}}
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
|
||||
<UButton @click="() => {
|
||||
toLangId = settingStore.shopDefaultLanguage
|
||||
isTranslations = false
|
||||
}" color="info" variant="outline">
|
||||
Cancel and back to Polish
|
||||
</UButton>
|
||||
<UButton color="info" @click="productStore.saveProductDescription(productID, toLangId)">
|
||||
Save translations
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="flex gap-7">
|
||||
<div v-if="isTranslations === false">
|
||||
<UButton @click="() => {
|
||||
fetchForLanguage(toLangId)
|
||||
isShowProductView = true
|
||||
}" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark)"
|
||||
color="neutral" variant="outline">
|
||||
<p class="dark:text-white"> Show product</p>
|
||||
</UButton>
|
||||
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) flex items-center! justify-center! px-12">
|
||||
Translate
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="flex gap-3">
|
||||
<UButton @click="() => {
|
||||
toLangId = settingStore.shopDefaultLanguage
|
||||
isTranslations = false
|
||||
}" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
|
||||
Cancel
|
||||
</UButton>
|
||||
<UButton color="primary" @click="productStore.saveProductDescription(productID, toLangId)"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
|
||||
Save
|
||||
</UButton>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</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">
|
||||
<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="">
|
||||
<div 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] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
|
||||
<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] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
|
||||
<p class="text-[18px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UTabs :items="items" v-model="activeTab" color="info" :content="true" :ui="{
|
||||
list : 'w-auto!'
|
||||
}">
|
||||
<template #description>
|
||||
<div v-if="!isTranslations" class="flex justify-end items-center gap-3 mb-4">
|
||||
<UButton v-if="!isEditing" @click="activeTab === 'usage' ? enableEdit() : 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="isEditing" @click="activeTab === 'usage' ? saveText : 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="isEditing" @click="activeTab === 'usage' ? cancelEdit : cancelDescriptionEdit"
|
||||
color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
Cancel
|
||||
</UButton>
|
||||
</div>
|
||||
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.description"
|
||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
|
||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
||||
<template #link>
|
||||
<EditorLinkPopover :editor="editor" auto-open />
|
||||
</template>
|
||||
</UEditorToolbar>
|
||||
</UEditor>
|
||||
<p v-else v-html="productStore.productDescription.description" class="text-black dark:text-white"></p>
|
||||
</template>
|
||||
|
||||
<template #usage>
|
||||
<div class="px-7">
|
||||
<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>
|
||||
|
||||
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.usage"
|
||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
|
||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
||||
<template #link>
|
||||
<EditorLinkPopover :editor="editor" auto-open />
|
||||
</template>
|
||||
</UEditorToolbar>
|
||||
</UEditor>
|
||||
<p v-else v-html="productStore.productDescription.usage" class="text-black dark:text-white"></p>
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEditable } from '@/composable/useConteditable';
|
||||
import Default from '@/layouts/default.vue';
|
||||
import { langs } from '@/router/langs';
|
||||
import { useProductStore } from '@/stores/product';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import type { EditorToolbarItem } from '@nuxt/ui';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const toolbarItems: EditorToolbarItem[][] = [
|
||||
// History controls
|
||||
[{
|
||||
kind: 'undo',
|
||||
icon: 'i-lucide-undo',
|
||||
tooltip: { text: 'Undo' }
|
||||
}, {
|
||||
kind: 'redo',
|
||||
icon: 'i-lucide-redo',
|
||||
tooltip: { text: 'Redo' }
|
||||
}],
|
||||
// Block types
|
||||
[{
|
||||
icon: 'i-lucide-heading',
|
||||
tooltip: { text: 'Headings' },
|
||||
content: {
|
||||
align: 'start'
|
||||
},
|
||||
items: [{
|
||||
kind: 'heading',
|
||||
level: 1,
|
||||
icon: 'i-lucide-heading-1',
|
||||
label: 'Heading 1'
|
||||
}, {
|
||||
kind: 'heading',
|
||||
level: 2,
|
||||
icon: 'i-lucide-heading-2',
|
||||
label: 'Heading 2'
|
||||
}, {
|
||||
kind: 'heading',
|
||||
level: 3,
|
||||
icon: 'i-lucide-heading-3',
|
||||
label: 'Heading 3'
|
||||
}, {
|
||||
kind: 'heading',
|
||||
level: 4,
|
||||
icon: 'i-lucide-heading-4',
|
||||
label: 'Heading 4'
|
||||
}]
|
||||
}, {
|
||||
icon: 'i-lucide-list',
|
||||
tooltip: { text: 'Lists' },
|
||||
content: {
|
||||
align: 'start'
|
||||
},
|
||||
items: [{
|
||||
kind: 'bulletList',
|
||||
icon: 'i-lucide-list',
|
||||
label: 'Bullet List'
|
||||
}, {
|
||||
kind: 'orderedList',
|
||||
icon: 'i-lucide-list-ordered',
|
||||
label: 'Ordered List'
|
||||
}]
|
||||
}, {
|
||||
kind: 'blockquote',
|
||||
icon: 'i-lucide-text-quote',
|
||||
tooltip: { text: 'Blockquote' }
|
||||
}, {
|
||||
kind: 'codeBlock',
|
||||
icon: 'i-lucide-square-code',
|
||||
tooltip: { text: 'Code Block' }
|
||||
}, {
|
||||
kind: 'horizontalRule',
|
||||
icon: 'i-lucide-separator-horizontal',
|
||||
tooltip: { text: 'Horizontal Rule' }
|
||||
}],
|
||||
// Text formatting
|
||||
[{
|
||||
kind: 'mark',
|
||||
mark: 'bold',
|
||||
icon: 'i-lucide-bold',
|
||||
tooltip: { text: 'Bold' }
|
||||
}, {
|
||||
kind: 'mark',
|
||||
mark: 'italic',
|
||||
icon: 'i-lucide-italic',
|
||||
tooltip: { text: 'Italic' }
|
||||
}, {
|
||||
kind: 'mark',
|
||||
mark: 'underline',
|
||||
icon: 'i-lucide-underline',
|
||||
tooltip: { text: 'Underline' }
|
||||
}, {
|
||||
kind: 'mark',
|
||||
mark: 'strike',
|
||||
icon: 'i-lucide-strikethrough',
|
||||
tooltip: { text: 'Strikethrough' }
|
||||
}, {
|
||||
kind: 'mark',
|
||||
mark: 'code',
|
||||
icon: 'i-lucide-code',
|
||||
tooltip: { text: 'Code' }
|
||||
}],
|
||||
// Link
|
||||
[{
|
||||
kind: 'link',
|
||||
icon: 'i-lucide-link',
|
||||
tooltip: { text: 'Link' }
|
||||
}],
|
||||
// Text alignment
|
||||
[{
|
||||
icon: 'i-lucide-align-justify',
|
||||
tooltip: { text: 'Text Align' },
|
||||
content: {
|
||||
align: 'end'
|
||||
},
|
||||
items: [{
|
||||
kind: 'textAlign',
|
||||
align: 'left',
|
||||
icon: 'i-lucide-align-left',
|
||||
label: 'Align Left'
|
||||
}, {
|
||||
kind: 'textAlign',
|
||||
align: 'center',
|
||||
icon: 'i-lucide-align-center',
|
||||
label: 'Align Center'
|
||||
}, {
|
||||
kind: 'textAlign',
|
||||
align: 'right',
|
||||
icon: 'i-lucide-align-right',
|
||||
label: 'Align Right'
|
||||
}, {
|
||||
kind: 'textAlign',
|
||||
align: 'justify',
|
||||
icon: 'i-lucide-align-justify',
|
||||
label: 'Align Justify'
|
||||
}]
|
||||
}]
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
function backFromProduct() {
|
||||
let path = localStorage.getItem('back_from_product')
|
||||
|
||||
if (path) {
|
||||
let res = JSON.parse(path)
|
||||
router.push({
|
||||
name: res.name,
|
||||
params: res.params,
|
||||
query: res.query
|
||||
})
|
||||
|
||||
localStorage.removeItem('back_from_product')
|
||||
} else {
|
||||
router.push({
|
||||
name: 'customer-products',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const settingStore = useSettingsStore()
|
||||
const productStore = useProductStore()
|
||||
const isTranslations = ref(false)
|
||||
|
||||
const toLangId = ref<number>(settingStore.shopDefaultLanguage)
|
||||
const productID = ref<number>(0)
|
||||
const translating = ref(false)
|
||||
|
||||
const activeTab = ref('description')
|
||||
const usageRef = ref<HTMLElement | null>(null)
|
||||
const originalUsage = ref('')
|
||||
const isEditing = ref(false)
|
||||
const usageEdit = useEditable(usageRef)
|
||||
const descriptionRef = ref<HTMLElement | null>(null)
|
||||
const descriptionEdit = useEditable(descriptionRef)
|
||||
const originalDescription = ref('')
|
||||
|
||||
const translateToSelectedLanguage = async () => {
|
||||
if (toLangId.value && productID.value) {
|
||||
translating.value = true
|
||||
try {
|
||||
await productStore.translateProductDescription(productID.value, toLangId.value)
|
||||
} finally {
|
||||
translating.value = false
|
||||
enableEdit()
|
||||
enableDescriptionEdit()
|
||||
}
|
||||
}
|
||||
if (productStore.productDescription) {
|
||||
isTranslations.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const fetchForLanguage = async (langId: number | null) => {
|
||||
if (productID.value) {
|
||||
await productStore.getProductDescription(langId, productID.value)
|
||||
removeInlineStylesFromAll(productStore.productDescription)
|
||||
}
|
||||
}
|
||||
|
||||
watch(toLangId, () => {
|
||||
fetchForLanguage(toLangId.value)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.product_id
|
||||
if (id) {
|
||||
productID.value = Number(id)
|
||||
await fetchForLanguage(toLangId.value)
|
||||
}
|
||||
})
|
||||
|
||||
const items = ref([
|
||||
{ label: 'Description', slot: 'description', name: 'description' },
|
||||
{ label: 'Usage', slot: 'usage', name: 'usage' }
|
||||
])
|
||||
|
||||
// text edit
|
||||
const enableEdit = () => {
|
||||
console.log(usageRef.value, 'usageRef');
|
||||
|
||||
if (usageRef.value) {
|
||||
originalUsage.value = usageRef.value.innerHTML
|
||||
}
|
||||
isEditing.value = true
|
||||
usageEdit.enableEdit()
|
||||
}
|
||||
|
||||
const enableDescriptionEdit = () => {
|
||||
console.log(usageRef.value, 'usageRef');
|
||||
|
||||
if (descriptionRef.value) {
|
||||
originalDescription.value = descriptionRef.value.innerHTML
|
||||
}
|
||||
descriptionEdit.enableEdit()
|
||||
}
|
||||
|
||||
const saveText = () => {
|
||||
if (usageRef.value) {
|
||||
productStore.productDescription.usage = usageRef.value.innerHTML
|
||||
}
|
||||
|
||||
usageEdit.disableEdit()
|
||||
isEditing.value = false
|
||||
|
||||
productStore.saveProductDescription(productID.value, toLangId.value)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
if (usageRef.value) {
|
||||
usageRef.value.innerHTML = originalUsage.value
|
||||
}
|
||||
usageEdit.disableEdit()
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const saveDescription = async () => {
|
||||
if (descriptionRef.value) {
|
||||
productStore.productDescription.description = descriptionRef.value.innerHTML
|
||||
}
|
||||
|
||||
descriptionEdit.disableEdit()
|
||||
|
||||
await productStore.saveProductDescription(productID.value, toLangId.value)
|
||||
toLangId.value = settingStore.shopDefaultLanguage
|
||||
isTranslations.value = false
|
||||
}
|
||||
|
||||
const cancelDescriptionEdit = () => {
|
||||
if (descriptionRef.value) {
|
||||
descriptionRef.value.innerHTML = originalDescription.value
|
||||
}
|
||||
descriptionEdit.disableEdit()
|
||||
}
|
||||
|
||||
function removeInlineStylesFromAll(product) {
|
||||
if (!product) return
|
||||
const removeStyles = (html: string) => {
|
||||
if (!html) return ''
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = html
|
||||
|
||||
div.querySelectorAll('*').forEach(el => {
|
||||
const bg = el.style.background || el.style.backgroundColor
|
||||
if (bg) {
|
||||
el.style.background = ''
|
||||
el.style.backgroundColor = ''
|
||||
}
|
||||
})
|
||||
div.querySelectorAll('p').forEach(p => {
|
||||
if (p.querySelector('img')) {
|
||||
p.style.display = 'flex'
|
||||
p.style.gap = '20px'
|
||||
p.style.padding = '30px 0'
|
||||
}
|
||||
})
|
||||
div.querySelectorAll('table').forEach(table => {
|
||||
table.querySelectorAll('tbody').forEach(tbody => {
|
||||
tbody.style.setProperty('background-color', '#F8FAFC', 'important')
|
||||
tbody.querySelectorAll('tr').forEach(tr => {
|
||||
tr.querySelectorAll('td').forEach(td => {
|
||||
td.style.setProperty('padding', '10px', 'important')
|
||||
td.style.setProperty('border-top', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-left', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-bottom', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-right', '1pt solid black', 'important')
|
||||
|
||||
td.querySelectorAll('p').forEach(p => {
|
||||
p.style.setProperty('display', 'flex', 'important')
|
||||
p.style.setProperty('padding', '0px', 'important')
|
||||
p.style.setProperty('align-items', 'start', 'important')
|
||||
p.style.setProperty('color', 'black', 'important')
|
||||
p.style.setProperty('font-size', '14px', 'important')
|
||||
p.querySelectorAll('span').forEach(span => {
|
||||
span.style.setProperty('color', 'black', 'important')
|
||||
span.style.setProperty('font-size', '14px', 'important')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
return div.innerHTML
|
||||
}
|
||||
product.description = removeStyles(product.description)
|
||||
product.description_short = removeStyles(product.description_short)
|
||||
product.usage = removeStyles(product.usage)
|
||||
}
|
||||
</script>
|
||||
@@ -5,7 +5,7 @@
|
||||
<p class="cursor-pointer text-(--text-sky-light) dark:text-(--text-sky-dark)" @click="backFromProduct()">
|
||||
Back to products</p>
|
||||
</div>
|
||||
<div class="container mx-auto ">
|
||||
<div>
|
||||
<div
|
||||
class="gap-4 bg-slate-50 dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -35,6 +35,10 @@
|
||||
</div>
|
||||
|
||||
<div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations" class="flex gap-7">
|
||||
<UButton color="info" variant="outline" v-if="!isEditing" @click="isTranslations = true">
|
||||
<p>Change Text</p>
|
||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-7.5" />
|
||||
</UButton>
|
||||
<UButton @click="translateToSelectedLanguage" color="info" :loading="translating">
|
||||
Translate from Polish to {{langs.find(l => l.id === toLangId)?.name}}
|
||||
</UButton>
|
||||
@@ -50,45 +54,9 @@
|
||||
Save translations
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="flex gap-7">
|
||||
<div v-if="isTranslations === false">
|
||||
<UButton @click="() => {
|
||||
fetchForLanguage(toLangId)
|
||||
isShowProductView = true
|
||||
}" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark)"
|
||||
color="neutral" variant="outline">
|
||||
<p class="dark:text-white"> Show product</p>
|
||||
</UButton>
|
||||
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
|
||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) flex items-center! justify-center! px-12">
|
||||
Translate
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="flex gap-3">
|
||||
<UButton @click="() => {
|
||||
toLangId = settingStore.shopDefaultLanguage
|
||||
isTranslations = false
|
||||
}" color="primary" class="text-(--text-sky-light) dark:text-(--text-sky-dark) bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
|
||||
Cancel
|
||||
</UButton>
|
||||
<UButton color="primary" @click="productStore.saveProductDescription(productID, toLangId)"
|
||||
class="text-(--text-sky-light) dark:text-(--text-sky-dark) bg-(--accent-blue-light) dark:bg-(--accent-blue-dark)">
|
||||
Save
|
||||
</UButton>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
@@ -96,29 +64,62 @@
|
||||
<p class="text-red-500">{{ productStore.error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="productStore.productDescription" class="">
|
||||
<div class="flex items-start gap-30 py-8">
|
||||
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<div v-else-if="productStore.productDescription" class="space-y-7.5">
|
||||
<div class="flex gap-6 h-full w-full">
|
||||
<div
|
||||
class="min-w-96 min-h-96 border border-(--border-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="flex flex-col justify-between min-h-full w-full">
|
||||
<div>
|
||||
<div>
|
||||
<p v-if="!isTranslations" class="text-[25px] font-bold text-black dark:text-white">
|
||||
{{ productStore.productDescription.name || 'Product Name' }}
|
||||
</p>
|
||||
<div class="" v-if="isTranslations">
|
||||
<p>Title:</p>
|
||||
<UEditor v-slot="{ editor }" v-model="productStore.productDescription.name" content-type="html"
|
||||
:ui="{ base: 'p-8 sm:px-16', root : 'pt-2' }" class="min-w-full border rounded-md border-(--border-light)"
|
||||
placeholder="Write there ...">
|
||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
||||
<template #link>
|
||||
<EditorLinkPopover :editor="editor" auto-open />
|
||||
</template>
|
||||
</UEditorToolbar>
|
||||
</UEditor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p v-if="!isTranslations" v-html="productStore.productDescription.description_short"
|
||||
class="text-black dark:text-white"></p>
|
||||
<div class="" v-if="isTranslations">
|
||||
<p>Short description:</p>
|
||||
<UEditor v-slot="{ editor }" v-model="productStore.productDescription.description_short"
|
||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }"
|
||||
class="w-full border rounded-md border-(--border-light)" placeholder="Write there ...">
|
||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
||||
<template #link>
|
||||
<EditorLinkPopover :editor="editor" auto-open />
|
||||
</template>
|
||||
</UEditorToolbar>
|
||||
</UEditor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-[10px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="lets-icons:done-ring-round-fill"
|
||||
class="text-[20px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
|
||||
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
|
||||
{{ productStore.productDescription.available_now }}
|
||||
<p>
|
||||
{{ productStore.productDescription.available_now || 'Available now' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon name="marketeq:car-shipping"
|
||||
class="text-[25px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
|
||||
<p class="text-[18px] font-bold text-black dark:text-white">
|
||||
<p>
|
||||
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -131,7 +132,7 @@
|
||||
root: 'items-start!'
|
||||
}">
|
||||
<template #description>
|
||||
<div v-if="!isTranslations" class="flex justify-end items-center gap-3 py-4">
|
||||
<!-- <div v-if="!isTranslations" class="flex justify-end items-center gap-3 mb-4">
|
||||
<UButton v-if="!isEditing" @click="activeTab === 'usage' ? enableEdit() : 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>
|
||||
@@ -145,7 +146,7 @@
|
||||
color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
||||
Cancel
|
||||
</UButton>
|
||||
</div>
|
||||
</div> -->
|
||||
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.description"
|
||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
|
||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
||||
@@ -367,13 +368,7 @@ const productID = ref<number>(0)
|
||||
const translating = ref(false)
|
||||
|
||||
const activeTab = ref('')
|
||||
const usageRef = ref<HTMLElement | null>(null)
|
||||
const originalUsage = ref('')
|
||||
const isEditing = ref(false)
|
||||
const usageEdit = useEditable(usageRef)
|
||||
const descriptionRef = ref<HTMLElement | null>(null)
|
||||
const descriptionEdit = useEditable(descriptionRef)
|
||||
const originalDescription = ref('')
|
||||
|
||||
const translateToSelectedLanguage = async () => {
|
||||
if (toLangId.value && productID.value) {
|
||||
@@ -382,8 +377,6 @@ const translateToSelectedLanguage = async () => {
|
||||
await productStore.translateProductDescription(productID.value, toLangId.value)
|
||||
} finally {
|
||||
translating.value = false
|
||||
enableEdit()
|
||||
enableDescriptionEdit()
|
||||
}
|
||||
}
|
||||
if (productStore.productDescription) {
|
||||
@@ -394,7 +387,6 @@ const translateToSelectedLanguage = async () => {
|
||||
const fetchForLanguage = async (langId: number | null) => {
|
||||
if (productID.value) {
|
||||
await productStore.getProductDescription(langId, productID.value)
|
||||
removeInlineStylesFromAll(productStore.productDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,119 +406,4 @@ const items = ref([
|
||||
{ label: 'Description', slot: 'description', name: 'description' },
|
||||
{ label: 'Usage', slot: 'usage', name: 'usage' }
|
||||
])
|
||||
|
||||
// text edit
|
||||
const enableEdit = () => {
|
||||
console.log(usageRef.value, 'usageRef');
|
||||
|
||||
if (usageRef.value) {
|
||||
originalUsage.value = usageRef.value.innerHTML
|
||||
}
|
||||
isEditing.value = true
|
||||
usageEdit.enableEdit()
|
||||
}
|
||||
|
||||
const enableDescriptionEdit = () => {
|
||||
console.log(usageRef.value, 'usageRef');
|
||||
|
||||
if (descriptionRef.value) {
|
||||
originalDescription.value = descriptionRef.value.innerHTML
|
||||
}
|
||||
descriptionEdit.enableEdit()
|
||||
}
|
||||
|
||||
const saveText = () => {
|
||||
if (usageRef.value) {
|
||||
productStore.productDescription.usage = usageRef.value.innerHTML
|
||||
}
|
||||
|
||||
usageEdit.disableEdit()
|
||||
isEditing.value = false
|
||||
|
||||
productStore.saveProductDescription(productID.value, toLangId.value)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
if (usageRef.value) {
|
||||
usageRef.value.innerHTML = originalUsage.value
|
||||
}
|
||||
usageEdit.disableEdit()
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const saveDescription = async () => {
|
||||
if (descriptionRef.value) {
|
||||
productStore.productDescription.description = descriptionRef.value.innerHTML
|
||||
}
|
||||
|
||||
descriptionEdit.disableEdit()
|
||||
|
||||
await productStore.saveProductDescription(productID.value, toLangId.value)
|
||||
toLangId.value = settingStore.shopDefaultLanguage
|
||||
isTranslations.value = false
|
||||
}
|
||||
|
||||
const cancelDescriptionEdit = () => {
|
||||
if (descriptionRef.value) {
|
||||
descriptionRef.value.innerHTML = originalDescription.value
|
||||
}
|
||||
descriptionEdit.disableEdit()
|
||||
}
|
||||
|
||||
function removeInlineStylesFromAll(product) {
|
||||
if (!product) return
|
||||
const removeStyles = (html: string) => {
|
||||
if (!html) return ''
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = html
|
||||
|
||||
const isDark = document.documentElement.classList.contains('dark')
|
||||
div.querySelectorAll('*').forEach(el => {
|
||||
const bg = el.style.background || el.style.backgroundColor
|
||||
if (bg) {
|
||||
el.style.background = ''
|
||||
el.style.backgroundColor = ''
|
||||
el.style.color = isDark ? 'white' : 'black'
|
||||
}
|
||||
})
|
||||
|
||||
div.querySelectorAll('p').forEach(p => {
|
||||
if (p.querySelector('img')) {
|
||||
p.style.display = 'flex'
|
||||
p.style.gap = '20px'
|
||||
p.style.padding = '30px 0'
|
||||
}
|
||||
})
|
||||
div.querySelectorAll('table').forEach(table => {
|
||||
table.querySelectorAll('tbody').forEach(tbody => {
|
||||
tbody.style.setProperty('background-color', '#F8FAFC', 'important')
|
||||
tbody.querySelectorAll('tr').forEach(tr => {
|
||||
tr.querySelectorAll('td').forEach(td => {
|
||||
td.style.setProperty('padding', '10px', 'important')
|
||||
td.style.setProperty('border-top', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-left', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-bottom', '1pt solid black', 'important')
|
||||
td.style.setProperty('border-right', '1pt solid black', 'important')
|
||||
|
||||
td.querySelectorAll('p').forEach(p => {
|
||||
p.style.setProperty('display', 'flex', 'important')
|
||||
p.style.setProperty('padding', '0px', 'important')
|
||||
p.style.setProperty('align-items', 'start', 'important')
|
||||
p.style.setProperty('color', 'black', 'important')
|
||||
p.style.setProperty('font-size', '14px', 'important')
|
||||
p.querySelectorAll('span').forEach(span => {
|
||||
span.style.setProperty('color', 'black', 'important')
|
||||
span.style.setProperty('font-size', '14px', 'important')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
return div.innerHTML
|
||||
}
|
||||
product.description = removeStyles(product.description)
|
||||
product.description_short = removeStyles(product.description_short)
|
||||
product.usage = removeStyles(product.usage)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,7 +41,7 @@ function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
adaptMenu(item.children);
|
||||
item.children.unshift({
|
||||
label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: {
|
||||
name: 'customer-products-category', params: {
|
||||
name: 'admin-products-category', params: {
|
||||
category_id: item.params.category_id,
|
||||
link_rewrite: item.params.link_rewrite
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function adaptMenu(menu: NavigationMenuItem[]) {
|
||||
})
|
||||
} else {
|
||||
item.to = {
|
||||
name: 'customer-products-category', params: {
|
||||
name: 'admin-products-category', params: {
|
||||
category_id: item.params.category_id,
|
||||
link_rewrite: item.params.link_rewrite
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<span class="text-xs text-gray-400">
|
||||
Country/Currency
|
||||
</span>
|
||||
<span class="font-medium dark:text-white text-black">
|
||||
{{ country?.name }} / {{ country?.ps_currency.iso_code }}
|
||||
<span v-if="country" class="font-medium dark:text-white text-black">
|
||||
{{ country?.name }} / {{ country?.ps_currency }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</template>
|
||||
|
||||
<template #default="{ state }">
|
||||
{{ menu }}
|
||||
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
||||
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
||||
</template>
|
||||
@@ -151,7 +152,7 @@ function getItems(state: 'collapsed' | 'expanded') {
|
||||
] satisfies NavigationMenuItem[]
|
||||
}
|
||||
|
||||
// zsdasdad
|
||||
//
|
||||
import { useRouter } from 'vue-router'
|
||||
import { currentLang } from '@/router/langs'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Change Locales
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Create Search Index
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Delete Index - MeiliSearch
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Search Index Settings
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Search Items
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
29
bruno/api_v1/auth/Login.yml
Normal file
29
bruno/api_v1/auth/Login.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
info:
|
||||
name: Login
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{bas_url}}/public/auth/login"
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"email":"{{email}}",
|
||||
"password":"{{password}}"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: email
|
||||
value: admin@ma-al.com
|
||||
- name: password
|
||||
value: Maal12345678
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/auth/folder.yml
Normal file
7
bruno/api_v1/auth/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: auth
|
||||
type: folder
|
||||
seq: 6
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
15
bruno/api_v1/currency/currency-rate.yml
Normal file
15
bruno/api_v1/currency/currency-rate.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: currency-rate
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{bas_url}}/restricted/currency-rate"
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
20
bruno/api_v1/currency/currency.yml
Normal file
20
bruno/api_v1/currency/currency.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
info:
|
||||
name: currency
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/currency-rate/{{id}}"
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
variables:
|
||||
- name: id
|
||||
value: "1"
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/currency/folder.yml
Normal file
7
bruno/api_v1/currency/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: currency
|
||||
type: folder
|
||||
seq: 8
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
19
bruno/api_v1/customer/Customer (me).yml
Normal file
19
bruno/api_v1/customer/Customer (me).yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: Customer (me)
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/customer?id=1"
|
||||
params:
|
||||
- name: id
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
19
bruno/api_v1/customer/Customer (other).yml
Normal file
19
bruno/api_v1/customer/Customer (other).yml
Normal file
@@ -0,0 +1,19 @@
|
||||
info:
|
||||
name: Customer (other)
|
||||
type: http
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/customer?id=1"
|
||||
params:
|
||||
- name: id
|
||||
value: "1"
|
||||
type: query
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/customer/folder.yml
Normal file
7
bruno/api_v1/customer/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: customer
|
||||
type: folder
|
||||
seq: 9
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
15
bruno/api_v1/product/Get Product.yml
Normal file
15
bruno/api_v1/product/Get Product.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
info:
|
||||
name: Get Product
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{bas_url}}/restricted/product/200/1/5"
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
bruno/api_v1/product/folder.yml
Normal file
7
bruno/api_v1/product/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: product
|
||||
type: folder
|
||||
seq: 7
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
@@ -1,7 +1,7 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TABLE IF NOT EXISTS b2b_language (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NULL,
|
||||
deleted_at DATETIME(6) NULL,
|
||||
@@ -19,29 +19,22 @@ CREATE TABLE IF NOT EXISTS b2b_language (
|
||||
CREATE INDEX IF NOT EXISTS idx_language_deleted_at
|
||||
ON b2b_language (deleted_at);
|
||||
|
||||
INSERT IGNORE INTO b2b_language
|
||||
(id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag)
|
||||
VALUES
|
||||
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
|
||||
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
|
||||
(3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS b2b_components (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_components_name ON b2b_components (name, id);
|
||||
-- scopes
|
||||
CREATE TABLE IF NOT EXISTS b2b_scopes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_scopes_name ON b2b_scopes (name);
|
||||
-- translations
|
||||
CREATE TABLE IF NOT EXISTS b2b_translations (
|
||||
lang_id INT NOT NULL,
|
||||
scope_id INT NOT NULL,
|
||||
component_id INT NOT NULL,
|
||||
lang_id INT UNSIGNED NOT NULL,
|
||||
scope_id INT UNSIGNED NOT NULL,
|
||||
component_id INT UNSIGNED NOT NULL,
|
||||
`key` VARCHAR(255) NOT NULL,
|
||||
data TEXT NULL,
|
||||
PRIMARY KEY (lang_id, scope_id, component_id, `key`),
|
||||
@@ -50,7 +43,49 @@ CREATE TABLE IF NOT EXISTS b2b_translations (
|
||||
CONSTRAINT fk_translations_component FOREIGN KEY (component_id) REFERENCES b2b_components(id) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `b2b_roles` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(63) NULL
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_permissions (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_role_permissions (
|
||||
role_id BIGINT UNSIGNED NOT NULL,
|
||||
permission_id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
|
||||
CONSTRAINT fk_role_permissions_role
|
||||
FOREIGN KEY (role_id)
|
||||
REFERENCES b2b_roles(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_role_permissions_permission
|
||||
FOREIGN KEY (permission_id)
|
||||
REFERENCES b2b_permissions(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE `b2b_top_menu_roles` (
|
||||
`top_menu_id` INT NOT NULL,
|
||||
`role_id` BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
PRIMARY KEY (`top_menu_id`, `role_id`),
|
||||
|
||||
CONSTRAINT fk_top_menu_roles_menu
|
||||
FOREIGN KEY (`top_menu_id`)
|
||||
REFERENCES `b2b_top_menu`(`menu_id`)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_top_menu_roles_role
|
||||
FOREIGN KEY (`role_id`)
|
||||
REFERENCES `b2b_roles`(`id`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- customers
|
||||
CREATE TABLE IF NOT EXISTS b2b_customers (
|
||||
@@ -59,7 +94,7 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
|
||||
password VARCHAR(255) NULL,
|
||||
first_name VARCHAR(100) NULL,
|
||||
last_name VARCHAR(100) NULL,
|
||||
role VARCHAR(20) NULL DEFAULT 'user',
|
||||
role_id BIGINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
provider VARCHAR(20) NULL DEFAULT 'local',
|
||||
provider_id VARCHAR(255) NULL,
|
||||
avatar_url VARCHAR(500) NULL,
|
||||
@@ -71,8 +106,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
|
||||
password_reset_expires DATETIME(6) NULL,
|
||||
last_password_reset_request DATETIME(6) NULL,
|
||||
last_login_at DATETIME(6) NULL,
|
||||
lang_id BIGINT NULL DEFAULT 2,
|
||||
country_id BIGINT NULL DEFAULT 2,
|
||||
lang_id INT NULL DEFAULT 2,
|
||||
country_id INT NULL DEFAULT 2,
|
||||
created_at DATETIME(6) NULL,
|
||||
updated_at DATETIME(6) NULL,
|
||||
deleted_at DATETIME(6) NULL
|
||||
@@ -84,10 +119,13 @@ ON b2b_customers (email);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_deleted_at
|
||||
ON b2b_customers (deleted_at);
|
||||
|
||||
ALTER TABLE b2b_customers
|
||||
ADD CONSTRAINT fk_customer_role
|
||||
FOREIGN KEY (role_id) REFERENCES b2b_roles(id);
|
||||
|
||||
-- customer_carts
|
||||
CREATE TABLE IF NOT EXISTS b2b_customer_carts (
|
||||
cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
cart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
@@ -97,10 +135,10 @@ CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (use
|
||||
|
||||
-- carts_products
|
||||
CREATE TABLE IF NOT EXISTS b2b_carts_products (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
cart_id BIGINT UNSIGNED NOT NULL,
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
cart_id INT UNSIGNED NOT NULL,
|
||||
product_id INT UNSIGNED NOT NULL,
|
||||
product_attribute_id BIGINT NULL,
|
||||
product_attribute_id INT NULL,
|
||||
amount INT UNSIGNED NOT NULL,
|
||||
CONSTRAINT fk_carts_products_customer_carts FOREIGN KEY (cart_id) REFERENCES b2b_customer_carts (cart_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_carts_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
@@ -110,7 +148,7 @@ CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (car
|
||||
|
||||
-- refresh_tokens
|
||||
CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_id BIGINT UNSIGNED NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL,
|
||||
expires_at DATETIME(6) NOT NULL,
|
||||
@@ -120,24 +158,133 @@ CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_tokens (token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
|
||||
|
||||
CREATE TABLE `b2b_currencies` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
`ps_id_currency` INT UNSIGNED NOT NULL,
|
||||
`is_default` TINYINT NOT NULL,
|
||||
`is_active` TINYINT NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
)
|
||||
ENGINE = InnoDB;
|
||||
|
||||
ALTER TABLE `b2b_currencies` ADD CONSTRAINT `FK_b2b_currencies_ps_id_currency` FOREIGN KEY (`ps_id_currency`) REFERENCES `ps_currency` (`id_currency`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX `fk_b2b_currencies_ps_currency`
|
||||
ON `b2b_currencies` (
|
||||
`ps_id_currency` ASC
|
||||
);
|
||||
|
||||
CREATE TABLE `b2b_currency_rates` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
`b2b_id_currency` BIGINT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`conversion_rate` DECIMAL(13,6) NULL DEFAULT NULL ,
|
||||
PRIMARY KEY (`id`)
|
||||
)
|
||||
ENGINE = InnoDB;
|
||||
ALTER TABLE `b2b_currency_rates` ADD CONSTRAINT `FK_b2b_currency_rates_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE INDEX `fk_b2b_currency_rates_b2b_currencies`
|
||||
ON `b2b_currency_rates` (
|
||||
`b2b_id_currency` ASC
|
||||
);
|
||||
|
||||
-- countries
|
||||
CREATE TABLE IF NOT EXISTS b2b_countries (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
currency_id INT UNSIGNED NOT NULL,
|
||||
flag VARCHAR(16) NOT NULL,
|
||||
CONSTRAINT fk_countries_currency FOREIGN KEY (currency_id) REFERENCES ps_currency(id_currency) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
`flag` VARCHAR(16) NOT NULL,
|
||||
`ps_id_country` INT UNSIGNED NOT NULL,
|
||||
`b2b_id_currency` BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `fk_b2b_countries_ps_country` FOREIGN KEY (`ps_id_country`) REFERENCES `ps_country` (`id_country`) ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT `FK_b2b_countries_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
|
||||
)
|
||||
ENGINE = InnoDB;
|
||||
CREATE INDEX `fk_b2b_countries_ps_country`
|
||||
ON `b2b_countries` (
|
||||
`ps_id_country` ASC
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO b2b_countries
|
||||
(id, name, currency_id, flag)
|
||||
VALUES
|
||||
(1, 'Polska', 1, '🇵🇱'),
|
||||
(2, 'England', 2, '🇬🇧'),
|
||||
(3, 'Čeština', 2, '🇨🇿'),
|
||||
(4, 'Deutschland', 2, '🇩🇪');
|
||||
CREATE TABLE b2b_specific_price (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NULL,
|
||||
updated_at DATETIME NULL,
|
||||
deleted_at DATETIME NULL,
|
||||
scope ENUM('shop', 'category', 'product') NOT NULL,
|
||||
valid_from DATETIME NULL,
|
||||
valid_till DATETIME NULL,
|
||||
has_expiration_date BOOLEAN DEFAULT FALSE,
|
||||
reduction_type ENUM('amount', 'percentage') NOT NULL,
|
||||
price DECIMAL(10, 2) NULL,
|
||||
b2b_id_currency BIGINT UNSIGNED NULL, -- specifies which currency is used for the price
|
||||
percentage_reduction DECIMAL(5, 2) NULL,
|
||||
from_quantity INT UNSIGNED DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
) ENGINE = InnoDB;
|
||||
CREATE INDEX idx_b2b_scope ON b2b_specific_price(scope);
|
||||
CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till);
|
||||
CREATE INDEX idx_b2b_lookup
|
||||
ON b2b_specific_price (
|
||||
scope,
|
||||
is_active,
|
||||
from_quantity
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_specific_price_product (
|
||||
b2b_specific_price_id BIGINT UNSIGNED,
|
||||
id_product INT UNSIGNED,
|
||||
PRIMARY KEY (b2b_specific_price_id, id_product),
|
||||
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (id_product) REFERENCES ps_product(id_product) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_specific_price_category (
|
||||
b2b_specific_price_id BIGINT UNSIGNED,
|
||||
id_category INT UNSIGNED,
|
||||
PRIMARY KEY (b2b_specific_price_id, id_category),
|
||||
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (id_category) REFERENCES ps_category(id_category) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_specific_price_product_attribute (
|
||||
b2b_specific_price_id BIGINT UNSIGNED,
|
||||
id_product_attribute INT UNSIGNED,
|
||||
PRIMARY KEY (b2b_specific_price_id, id_product_attribute),
|
||||
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (id_product_attribute) REFERENCES ps_product_attribute(id_product_attribute) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_specific_price_customer (
|
||||
b2b_specific_price_id BIGINT UNSIGNED,
|
||||
b2b_id_customer BIGINT UNSIGNED,
|
||||
PRIMARY KEY (b2b_specific_price_id, b2b_id_customer),
|
||||
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (b2b_id_customer) REFERENCES b2b_customers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE b2b_specific_price_country (
|
||||
b2b_specific_price_id BIGINT UNSIGNED,
|
||||
b2b_id_country BIGINT UNSIGNED,
|
||||
PRIMARY KEY (b2b_specific_price_id, b2b_id_country),
|
||||
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (b2b_id_country) REFERENCES b2b_countries(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_b2b_product_rel
|
||||
ON b2b_specific_price_product (id_product);
|
||||
|
||||
CREATE INDEX idx_b2b_category_rel
|
||||
ON b2b_specific_price_category (id_category);
|
||||
|
||||
CREATE INDEX idx_b2b_product_attribute_rel
|
||||
ON b2b_specific_price_product_attribute (id_product_attribute);
|
||||
|
||||
CREATE INDEX idx_bsp_customer
|
||||
ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer);
|
||||
|
||||
CREATE INDEX idx_bsp_country
|
||||
ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country);
|
||||
|
||||
DELIMITER //
|
||||
|
||||
@@ -238,3 +385,9 @@ DROP TABLE IF EXISTS b2b_scopes;
|
||||
DROP TABLE IF EXISTS b2b_translations;
|
||||
DROP TABLE IF EXISTS b2b_customers;
|
||||
DROP TABLE IF EXISTS b2b_refresh_tokens;
|
||||
DROP TABLE IF EXISTS b2b_currencies;
|
||||
DROP TABLE IF EXISTS b2b_currency_rates;
|
||||
DROP TABLE IF EXISTS b2b_specific_price;
|
||||
DROP TABLE IF EXISTS b2b_specific_price_product;
|
||||
DROP TABLE IF EXISTS b2b_specific_price_category;
|
||||
DROP TABLE IF EXISTS b2b_specific_price_product_attribute;
|
||||
|
||||
35
i18n/migrations/20260302163123_create_tables_data.sql
Normal file
35
i18n/migrations/20260302163123_create_tables_data.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- +goose Up
|
||||
|
||||
INSERT IGNORE INTO b2b_language
|
||||
(id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag)
|
||||
VALUES
|
||||
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
|
||||
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
|
||||
(3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
|
||||
|
||||
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('user','1');
|
||||
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('admin','2');
|
||||
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3');
|
||||
|
||||
|
||||
-- insert sample admin user admin@ma-al.com/Maal12345678
|
||||
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role_id, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
|
||||
VALUES
|
||||
(1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 2, 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 1, 1, '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL);
|
||||
ALTER TABLE b2b_customers AUTO_INCREMENT = 1;
|
||||
|
||||
INSERT INTO `b2b_currencies` (`ps_id_currency`, `is_default`, `is_active`) VALUES
|
||||
('1','1','1'),
|
||||
('2','0','1');
|
||||
|
||||
INSERT IGNORE INTO b2b_countries
|
||||
(id, flag, ps_id_country, b2b_id_currency)
|
||||
VALUES
|
||||
(1, '🇵🇱', 14, 1),
|
||||
(2, '🇬🇧', 17, 2),
|
||||
(3, '🇨🇿', 16, 2),
|
||||
(4, '🇩🇪', 1, 2);
|
||||
|
||||
|
||||
|
||||
-- +goose Down
|
||||
380
i18n/migrations/20260319163200_procedures.sql
Normal file
380
i18n/migrations/20260319163200_procedures.sql
Normal file
@@ -0,0 +1,380 @@
|
||||
-- +goose Up
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS get_full_product
|
||||
//
|
||||
CREATE PROCEDURE get_full_product(
|
||||
IN p_id_product INT UNSIGNED,
|
||||
IN p_id_shop INT UNSIGNED,
|
||||
IN p_id_lang INT UNSIGNED,
|
||||
IN p_id_customer INT UNSIGNED,
|
||||
IN b2b_id_country INT UNSIGNED,
|
||||
IN p_quantity INT UNSIGNED
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_tax_rate DECIMAL(10, 4) DEFAULT 0;
|
||||
DECLARE p_id_currency DECIMAL(10, 4) DEFAULT 0;
|
||||
SELECT
|
||||
COALESCE(t.rate, 0.0000) INTO v_tax_rate
|
||||
FROM
|
||||
ps_tax_rule tr
|
||||
INNER JOIN ps_tax t ON t.id_tax = tr.id_tax
|
||||
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
|
||||
WHERE
|
||||
tr.id_tax_rules_group = (
|
||||
SELECT
|
||||
ps.id_tax_rules_group
|
||||
FROM
|
||||
ps_product_shop ps
|
||||
WHERE
|
||||
ps.id_product = p_id_product
|
||||
AND ps.id_shop = p_id_shop
|
||||
LIMIT
|
||||
1
|
||||
)
|
||||
AND tr.id_country = b2b_countries.ps_id_country
|
||||
ORDER BY
|
||||
tr.id_state DESC,
|
||||
tr.zipcode_from != '' DESC,
|
||||
tr.id_tax_rule DESC
|
||||
LIMIT
|
||||
1;
|
||||
|
||||
SELECT
|
||||
b2b_currencies.ps_id_currency INTO p_id_currency
|
||||
FROM
|
||||
b2b_currencies
|
||||
LEFT JOIN b2b_countries ON b2b_countries.b2b_id_currency = b2b_currencies.id
|
||||
WHERE
|
||||
b2b_countries.id = b2b_id_country
|
||||
LIMIT 1;
|
||||
|
||||
/* FINAL JSON */
|
||||
SELECT
|
||||
JSON_OBJECT(
|
||||
/* ================= PRODUCT ================= */
|
||||
'id_product',
|
||||
p.id_product,
|
||||
'reference',
|
||||
p.reference,
|
||||
'name',
|
||||
pl.name,
|
||||
'description',
|
||||
pl.description,
|
||||
'short_description',
|
||||
pl.description_short,
|
||||
/* ================= PRICE ================= */
|
||||
'price',
|
||||
JSON_OBJECT(
|
||||
'base',
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate),
|
||||
|
||||
'final_tax_excl',
|
||||
(
|
||||
CASE
|
||||
WHEN bsp.id IS NOT NULL THEN
|
||||
CASE
|
||||
/* FIXED PRICE */
|
||||
WHEN bsp.reduction_type = 'amount' THEN
|
||||
(
|
||||
CASE
|
||||
WHEN bsp.b2b_id_currency IS NULL THEN bsp.price
|
||||
ELSE bsp.price * br_bsp.conversion_rate
|
||||
END
|
||||
)
|
||||
|
||||
/* PERCENTAGE */
|
||||
WHEN bsp.reduction_type = 'percentage' THEN
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
* (1 - bsp.percentage_reduction / 100)
|
||||
|
||||
ELSE
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
END
|
||||
|
||||
ELSE
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
END
|
||||
),
|
||||
|
||||
'final_tax_incl',
|
||||
(
|
||||
(
|
||||
CASE
|
||||
WHEN bsp.id IS NOT NULL THEN
|
||||
CASE
|
||||
WHEN bsp.reduction_type = 'amount' THEN
|
||||
(
|
||||
CASE
|
||||
WHEN bsp.b2b_id_currency IS NULL THEN bsp.price
|
||||
ELSE bsp.price * br_bsp.conversion_rate
|
||||
END
|
||||
)
|
||||
|
||||
WHEN bsp.reduction_type = 'percentage' THEN
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
* (1 - bsp.percentage_reduction / 100)
|
||||
|
||||
ELSE
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
END
|
||||
ELSE
|
||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
||||
END
|
||||
) * (1 + v_tax_rate / 100)
|
||||
)
|
||||
),
|
||||
/* ================= META ================= */
|
||||
'active',
|
||||
COALESCE(ps.active, p.active),
|
||||
'visibility',
|
||||
COALESCE(ps.visibility, p.visibility),
|
||||
'manufacturer',
|
||||
m.name,
|
||||
'category',
|
||||
cl.name,
|
||||
/* ================= IMAGE ================= */
|
||||
'cover_image',
|
||||
JSON_OBJECT(
|
||||
'id',
|
||||
i.id_image,
|
||||
'legend',
|
||||
il.legend
|
||||
),
|
||||
/* ================= FEATURES ================= */
|
||||
'features',
|
||||
(
|
||||
SELECT
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'name',
|
||||
fl.name,
|
||||
'value',
|
||||
fvl.value
|
||||
)
|
||||
)
|
||||
FROM
|
||||
ps_feature_product fp
|
||||
JOIN ps_feature_lang fl ON fl.id_feature = fp.id_feature
|
||||
AND fl.id_lang = p_id_lang
|
||||
JOIN ps_feature_value_lang fvl ON fvl.id_feature_value = fp.id_feature_value
|
||||
AND fvl.id_lang = p_id_lang
|
||||
WHERE
|
||||
fp.id_product = p.id_product
|
||||
),
|
||||
/* ================= COMBINATIONS ================= */
|
||||
'combinations',
|
||||
(
|
||||
SELECT
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id_product_attribute',
|
||||
pa.id_product_attribute,
|
||||
'reference',
|
||||
pa.reference,
|
||||
'price',
|
||||
JSON_OBJECT(
|
||||
'impact',
|
||||
COALESCE(pas.price, pa.price),
|
||||
'final_tax_excl',
|
||||
(
|
||||
COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price)
|
||||
),
|
||||
'final_tax_incl',
|
||||
(
|
||||
(
|
||||
COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price)
|
||||
) * (1 + v_tax_rate / 100)
|
||||
)
|
||||
),
|
||||
'stock',
|
||||
IFNULL(sa.quantity, 0),
|
||||
'default_on',
|
||||
pas.default_on,
|
||||
/* ATTRIBUTES JSON */
|
||||
'attributes',
|
||||
(
|
||||
SELECT
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'group',
|
||||
agl.name,
|
||||
'attribute',
|
||||
al.name
|
||||
)
|
||||
)
|
||||
FROM
|
||||
ps_product_attribute_combination pac
|
||||
JOIN ps_attribute a ON a.id_attribute = pac.id_attribute
|
||||
JOIN ps_attribute_lang al ON al.id_attribute = a.id_attribute
|
||||
AND al.id_lang = p_id_lang
|
||||
JOIN ps_attribute_group_lang agl ON agl.id_attribute_group = a.id_attribute_group
|
||||
AND agl.id_lang = p_id_lang
|
||||
WHERE
|
||||
pac.id_product_attribute = pa.id_product_attribute
|
||||
),
|
||||
/* IMAGES */
|
||||
'images',
|
||||
(
|
||||
SELECT
|
||||
JSON_ARRAYAGG(img.id_image)
|
||||
FROM
|
||||
ps_product_attribute_image pai
|
||||
JOIN ps_image img ON img.id_image = pai.id_image
|
||||
WHERE
|
||||
pai.id_product_attribute = pa.id_product_attribute
|
||||
)
|
||||
)
|
||||
)
|
||||
FROM
|
||||
ps_product_attribute pa
|
||||
JOIN ps_product_attribute_shop pas ON pas.id_product_attribute = pa.id_product_attribute
|
||||
AND pas.id_shop = p_id_shop
|
||||
LEFT JOIN ps_stock_available sa ON sa.id_product = pa.id_product
|
||||
AND sa.id_product_attribute = pa.id_product_attribute
|
||||
AND sa.id_shop = p_id_shop
|
||||
WHERE
|
||||
pa.id_product = p.id_product
|
||||
)
|
||||
) AS product_json
|
||||
FROM
|
||||
ps_product p
|
||||
LEFT JOIN ps_product_shop ps ON ps.id_product = p.id_product
|
||||
AND ps.id_shop = p_id_shop
|
||||
LEFT JOIN ps_product_lang pl ON pl.id_product = p.id_product
|
||||
AND pl.id_lang = p_id_lang
|
||||
AND pl.id_shop = p_id_shop
|
||||
LEFT JOIN ps_category_lang cl ON cl.id_category = COALESCE(ps.id_category_default, p.id_category_default)
|
||||
AND cl.id_lang = p_id_lang
|
||||
AND cl.id_shop = p_id_shop
|
||||
LEFT JOIN ps_manufacturer m ON m.id_manufacturer = p.id_manufacturer
|
||||
LEFT JOIN ps_image i ON i.id_product = p.id_product
|
||||
AND i.cover = 1
|
||||
LEFT JOIN ps_image_lang il ON il.id_image = i.id_image
|
||||
AND il.id_lang = p_id_lang
|
||||
/* SPECIFIC PRICE */
|
||||
LEFT JOIN (
|
||||
SELECT bsp.*
|
||||
FROM b2b_specific_price bsp
|
||||
|
||||
/* RELATIONS */
|
||||
LEFT JOIN b2b_specific_price_product bsp_p
|
||||
ON bsp_p.b2b_specific_price_id = bsp.id
|
||||
|
||||
LEFT JOIN b2b_specific_price_category bsp_c
|
||||
ON bsp_c.b2b_specific_price_id = bsp.id
|
||||
|
||||
WHERE bsp.is_active = TRUE
|
||||
|
||||
/* SCOPE MATCH */
|
||||
AND (
|
||||
/* PRODUCT */
|
||||
(bsp.scope = 'product' AND bsp_p.id_product = p_id_product)
|
||||
|
||||
/* CATEGORY */
|
||||
OR (
|
||||
bsp.scope = 'category'
|
||||
AND bsp_c.id_category IN (
|
||||
SELECT cp.id_category
|
||||
FROM ps_category_product cp
|
||||
WHERE cp.id_product = p_id_product
|
||||
)
|
||||
)
|
||||
|
||||
/* SHOP (GLOBAL) */
|
||||
OR (bsp.scope = 'shop')
|
||||
)
|
||||
|
||||
/* CUSTOMER MATCH */
|
||||
AND (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_customer c
|
||||
WHERE c.b2b_specific_price_id = bsp.id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_customer c
|
||||
WHERE c.b2b_specific_price_id = bsp.id
|
||||
AND c.b2b_id_customer = p_id_customer
|
||||
)
|
||||
)
|
||||
|
||||
/* COUNTRY MATCH */
|
||||
AND (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_country ctry
|
||||
WHERE ctry.b2b_specific_price_id = bsp.id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_country ctry
|
||||
WHERE ctry.b2b_specific_price_id = bsp.id
|
||||
AND ctry.b2b_id_country = b2b_id_country
|
||||
)
|
||||
)
|
||||
|
||||
/* QUANTITY */
|
||||
AND bsp.from_quantity <= p_quantity
|
||||
|
||||
/* DATE */
|
||||
AND (
|
||||
bsp.has_expiration_date = FALSE OR (
|
||||
(bsp.valid_from IS NULL OR bsp.valid_from <= NOW())
|
||||
AND (bsp.valid_till IS NULL OR bsp.valid_till >= NOW())
|
||||
)
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
/* 🔥 SCOPE PRIORITY */
|
||||
bsp.scope = 'product' DESC,
|
||||
bsp.scope = 'category' DESC,
|
||||
bsp.scope = 'shop' DESC,
|
||||
|
||||
/* 🔥 CUSTOMER PRIORITY */
|
||||
(
|
||||
EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_customer c
|
||||
WHERE c.b2b_specific_price_id = bsp.id
|
||||
AND c.b2b_id_customer = p_id_customer
|
||||
)
|
||||
) DESC,
|
||||
|
||||
/* 🔥 COUNTRY PRIORITY */
|
||||
(
|
||||
EXISTS (
|
||||
SELECT 1 FROM b2b_specific_price_country ctry
|
||||
WHERE ctry.b2b_specific_price_id = bsp.id
|
||||
AND ctry.b2b_id_country = b2b_id_country
|
||||
)
|
||||
) DESC,
|
||||
|
||||
/* GLOBAL fallback (no restrictions) naturally goes last */
|
||||
|
||||
bsp.from_quantity DESC,
|
||||
bsp.id DESC
|
||||
|
||||
LIMIT 1
|
||||
) bsp ON 1=1
|
||||
LEFT JOIN b2b_currency_rates br_bsp
|
||||
ON br_bsp.b2b_id_currency = bsp.b2b_id_currency
|
||||
AND br_bsp.created_at = (
|
||||
SELECT MAX(created_at)
|
||||
FROM b2b_currency_rates
|
||||
WHERE b2b_id_currency = bsp.b2b_id_currency
|
||||
)
|
||||
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
|
||||
LEFT JOIN b2b_currencies ON b2b_currencies.id = p_id_currency
|
||||
LEFT JOIN b2b_currency_rates r ON r.b2b_id_currency = b2b_currencies.id
|
||||
AND r.created_at = (
|
||||
SELECT
|
||||
MAX(created_at)
|
||||
FROM
|
||||
b2b_currency_rates
|
||||
WHERE
|
||||
b2b_id_currency = b2b_currencies.id
|
||||
)
|
||||
WHERE
|
||||
p.id_product = p_id_product
|
||||
LIMIT
|
||||
1;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
-- +goose Down
|
||||
9
i18n/migrations/20260320113729_stuff.sql
Normal file
9
i18n/migrations/20260320113729_stuff.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
SELECT 'up SQL query';
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
53
repository/currencyRepo/currencyRepo.go
Normal file
53
repository/currencyRepo/currencyRepo.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package currencyRepo
|
||||
|
||||
import (
|
||||
"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/utils/query/filters"
|
||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||
)
|
||||
|
||||
type UICurrencyRepo interface {
|
||||
CreateConversionRate(currencyRate *model.CurrencyRate) error
|
||||
Get(id uint) (*model.Currency, error)
|
||||
}
|
||||
|
||||
type CurrencyRepo struct{}
|
||||
|
||||
func New() UICurrencyRepo {
|
||||
return &CurrencyRepo{}
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error {
|
||||
return db.DB.Debug().Create(currencyRate).Error
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
|
||||
var currency model.Currency
|
||||
|
||||
err := db.DB.Table("b2b_currencies c").
|
||||
Select("c.*, r.conversion_rate").
|
||||
Joins(`
|
||||
LEFT JOIN b2b_currency_rates r
|
||||
ON r.b2b_id_currency = c.id
|
||||
AND r.created_at = (
|
||||
SELECT MAX(created_at)
|
||||
FROM b2b_currency_rates
|
||||
WHERE b2b_id_currency = c.id
|
||||
)
|
||||
`).
|
||||
Where("c.id = ?", id).
|
||||
Scan(¤cy).Error
|
||||
|
||||
return ¤cy, err
|
||||
}
|
||||
|
||||
func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
|
||||
|
||||
found, err := find.Paginate[model.Currency](langId, p, db.DB.
|
||||
Model(&model.Currency{}).
|
||||
Scopes(filt.All()...),
|
||||
)
|
||||
|
||||
return &found, err
|
||||
}
|
||||
@@ -59,6 +59,7 @@ tasks:
|
||||
- |
|
||||
sed '/-- +goose Down/,$d' i18n/migrations/20260302163100_routes.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
|
||||
sed '/-- +goose Down/,$d' i18n/migrations/20260302163122_create_tables.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
|
||||
sed '/-- +goose Down/,$d' i18n/migrations/20260302163123_create_tables_data.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
|
||||
sed '/-- +goose Down/,$d' i18n/migrations/20260302163152_translations_backoffice.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
|
||||
sed '/-- +goose Down/,$d' i18n/migrations/20260302163157_translations_backend.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user