Compare commits
36 Commits
mailisearc
...
routing
| Author | SHA1 | Date | |
|---|---|---|---|
| e279899e49 | |||
| e7adddb927 | |||
| 62aaa23164 | |||
| 0f21ed1f81 | |||
| c13365916c | |||
| a96efacb3e | |||
| e570297011 | |||
| bb1cdee3f4 | |||
|
|
d6fa655c21 | ||
|
|
defdfc7b06 | ||
|
|
c464c02301 | ||
| bdaf00ce45 | |||
| 73cc936d56 | |||
| 3b80fa6dbe | |||
| f773eed2d2 | |||
| 7de369e46a | |||
| 0cee3e5cb7 | |||
| 78b32cbd9c | |||
|
|
04538d4373 | ||
| 32a651064e | |||
|
|
bfc488bad8 | ||
|
|
f5f23f8a27 | ||
| d0c1f49d3e | |||
| cffe4c2f83 | |||
| 15e8626280 | |||
|
|
528f12b065 | ||
| 508418523f | |||
| 0853424c4e | |||
| 12e9e49f9b | |||
|
|
25ad592be3 | ||
| a984d2ac0d | |||
|
|
26e6a3c384 | ||
| a4c1773415 | |||
| f60d1bb6de | |||
| 95b73b9836 | |||
| 99fe11fbeb |
11
.env
11
.env
@@ -21,6 +21,13 @@ AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
|
|||||||
AUTH_JWT_EXPIRATION=86400
|
AUTH_JWT_EXPIRATION=86400
|
||||||
AUTH_REFRESH_EXPIRATION=604800
|
AUTH_REFRESH_EXPIRATION=604800
|
||||||
|
|
||||||
|
# Meili search
|
||||||
|
MEILISEARCH_URL=http://localhost:7700
|
||||||
|
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_KEY=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
|
||||||
|
|
||||||
# Google Translate Client
|
# Google Translate Client
|
||||||
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
||||||
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
||||||
@@ -29,6 +36,7 @@ GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
|||||||
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
||||||
OAUTH_GOOGLE_CLIENT_SECRET=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
|
OAUTH_GOOGLE_CLIENT_SECRET=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
|
||||||
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
||||||
|
|
||||||
# Email Configuration (SMTP)
|
# Email Configuration (SMTP)
|
||||||
# Set EMAIL_ENABLED=true to require email verification
|
# Set EMAIL_ENABLED=true to require email verification
|
||||||
EMAIL_ENABLED=true
|
EMAIL_ENABLED=true
|
||||||
@@ -47,3 +55,6 @@ PDF_SERVER_URL=http://localhost:8000
|
|||||||
|
|
||||||
FILE_MAAL_PL_USER=git_operator
|
FILE_MAAL_PL_USER=git_operator
|
||||||
FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
||||||
|
|
||||||
|
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
|
||||||
|
CORS_ORGIN=https://www.naluconcept.com
|
||||||
59
.env_example
Normal file
59
.env_example
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
SERVER_PORT=3000
|
||||||
|
SERVER_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=Maal12345678
|
||||||
|
DB_NAME=nalu
|
||||||
|
PROJECT_NAME=nalu_b2b
|
||||||
|
DB_SERVICE_NAME=nalu_b2b
|
||||||
|
DB_SSLMODE=disable
|
||||||
|
|
||||||
|
# App COnfig
|
||||||
|
APP_NAME="B2b Management System"
|
||||||
|
APP_VERSION=2.1.0
|
||||||
|
APP_ENVIRONMENT=development
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
AUTH_JWT_SECRET=5c020e6ed3d8d6e67e5804d67c83c4bd5ae474df749af6d63d8f20e7e2ba29b3
|
||||||
|
AUTH_JWT_EXPIRATION=86400
|
||||||
|
AUTH_REFRESH_EXPIRATION=604800
|
||||||
|
|
||||||
|
# Meili search
|
||||||
|
MEILISEARCH_URL=http://localhost:7700
|
||||||
|
MEILISEARCH_API_KEY=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_KEY=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
|
||||||
|
|
||||||
|
# Google Translate Client
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS=./google-cred.json
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID=translation-343517
|
||||||
|
|
||||||
|
# Google OAuth2
|
||||||
|
OAUTH_GOOGLE_CLIENT_ID=331979954218-9vrpe08oqhhcgj6bvu6d4lds0dt630m9.apps.googleusercontent.com
|
||||||
|
OAUTH_GOOGLE_CLIENT_SECRET=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
|
||||||
|
OAUTH_GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/public/auth/google/callback
|
||||||
|
|
||||||
|
# Email Configuration (SMTP)
|
||||||
|
# Set EMAIL_ENABLED=true to require email verification
|
||||||
|
EMAIL_ENABLED=true
|
||||||
|
EMAIL_SMTP_HOST=mail.ma-al.com
|
||||||
|
EMAIL_SMTP_PORT=587
|
||||||
|
EMAIL_SMTP_USER=test@ma-al.com
|
||||||
|
EMAIL_SMTP_PASSWORD=maal12345678
|
||||||
|
EMAIL_FROM=test@ma-al.com
|
||||||
|
EMAIL_FROM_NAME=Gitea Manager
|
||||||
|
EMAIL_ADMIN=goc_marek@ma-al.pl
|
||||||
|
|
||||||
|
I18N_LANGS=en,pl,cs
|
||||||
|
|
||||||
|
PDF_SERVER_URL=http://localhost:8000
|
||||||
|
|
||||||
|
|
||||||
|
FILE_MAAL_PL_USER=git_operator
|
||||||
|
FILE_MAAL_PL_PASSWORD=1FnwqcEgIUjQHjt1
|
||||||
|
|
||||||
|
IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same host as presta
|
||||||
1619
app/api/openapi.json
1619
app/api/openapi.json
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,8 @@ type Config struct {
|
|||||||
I18n I18n
|
I18n I18n
|
||||||
Pdf PdfPrinter
|
Pdf PdfPrinter
|
||||||
GoogleTranslate GoogleTranslateConfig
|
GoogleTranslate GoogleTranslateConfig
|
||||||
|
Image ImageConfig
|
||||||
|
Cors CorsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type I18n struct {
|
type I18n struct {
|
||||||
@@ -32,6 +34,14 @@ type ServerConfig struct {
|
|||||||
Host string `env:"SERVER_HOST,0.0.0.0"`
|
Host string `env:"SERVER_HOST,0.0.0.0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CorsConfig struct {
|
||||||
|
Origins []string `env:"CORS_ORGIN"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageConfig struct {
|
||||||
|
ImagePrefix string `env:"IMAGE_PREFIX"`
|
||||||
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Host string `env:"DB_HOST,localhost"`
|
Host string `env:"DB_HOST,localhost"`
|
||||||
Port int `env:"DB_PORT"`
|
Port int `env:"DB_PORT"`
|
||||||
@@ -167,6 +177,15 @@ func load() *Config {
|
|||||||
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = loadEnv(&cfg.Image)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = loadEnv(&cfg.Cors)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v3"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
// CORSMiddleware creates CORS middleware
|
// CORSMiddleware creates CORS middleware
|
||||||
func CORSMiddleware() fiber.Handler {
|
func CORSMiddleware() fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
c.Set("Access-Control-Allow-Origin", "*")
|
|
||||||
|
if strings.Contains(c.Get("Host"), "localhost") {
|
||||||
|
c.Set("Access-Control-Allow-Origin", c.Get("Host"))
|
||||||
|
} else {
|
||||||
|
origins := strings.Join(config.Get().Cors.Origins, ",")
|
||||||
|
c.Set("Access-Control-Allow-Origin", origins)
|
||||||
|
}
|
||||||
|
|
||||||
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
|
|||||||
168
app/delivery/web/api/restricted/carts.go
Normal file
168
app/delivery/web/api/restricted/carts.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/cartsService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CartsHandler handles endpoints that modify carts.
|
||||||
|
type CartsHandler struct {
|
||||||
|
cartsService *cartsService.CartsService
|
||||||
|
}
|
||||||
|
|
||||||
|
// CartsHandler creates a new CartsHandler instance
|
||||||
|
func NewCartsHandler() *CartsHandler {
|
||||||
|
cartsService := cartsService.New()
|
||||||
|
return &CartsHandler{
|
||||||
|
cartsService: cartsService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CartsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewCartsHandler()
|
||||||
|
|
||||||
|
r.Get("/add-new-cart", handler.AddNewCart)
|
||||||
|
r.Get("/change-cart-name", handler.ChangeCartName)
|
||||||
|
r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo)
|
||||||
|
r.Get("/retrieve-cart", handler.RetrieveCart)
|
||||||
|
r.Get("/add-product-to-cart", handler.AddProduct)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("userID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
new_cart, err := h.cartsService.CreateNewCart(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&new_cart, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("userID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
new_name := c.Query("new_name")
|
||||||
|
|
||||||
|
cart_id_attribute := c.Query("cart_id")
|
||||||
|
cart_id, err := strconv.Atoi(cart_id_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("userID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
carts_info, err := h.cartsService.RetrieveCartsInfo(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&carts_info, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("userID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
cart_id_attribute := c.Query("cart_id")
|
||||||
|
cart_id, err := strconv.Atoi(cart_id_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
cart, err := h.cartsService.RetrieveCart(userID, uint(cart_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(cart, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("userID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
cart_id_attribute := c.Query("cart_id")
|
||||||
|
cart_id, err := strconv.Atoi(cart_id_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
product_id_attribute := c.Query("product_id")
|
||||||
|
product_id, err := strconv.Atoi(product_id_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
product_attribute_id_attribute := c.Query("product_attribute_id")
|
||||||
|
var product_attribute_id *uint
|
||||||
|
if product_attribute_id_attribute == "" {
|
||||||
|
product_attribute_id = nil
|
||||||
|
} else {
|
||||||
|
val, err := strconv.Atoi(product_attribute_id_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
uval := uint(val)
|
||||||
|
product_attribute_id = &uval
|
||||||
|
}
|
||||||
|
|
||||||
|
amount_attribute := c.Query("amount")
|
||||||
|
amount, err := strconv.Atoi(amount_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, uint(amount))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package restricted
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsAndCountriesService"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LangsAndCountriesHandler for getting languages and countries data
|
|
||||||
type LangsAndCountriesHandler struct {
|
|
||||||
langsAndCountriesService *langsAndCountriesService.LangsAndCountriesService
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLangsAndCountriesHandler creates a new LangsAndCountriesHandler instance
|
|
||||||
func NewLangsAndCountriesHandler() *LangsAndCountriesHandler {
|
|
||||||
langsAndCountriesService := langsAndCountriesService.New()
|
|
||||||
return &LangsAndCountriesHandler{
|
|
||||||
langsAndCountriesService: langsAndCountriesService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LangsAndCountriesHandlerRoutes(r fiber.Router) fiber.Router {
|
|
||||||
handler := NewLangsAndCountriesHandler()
|
|
||||||
|
|
||||||
r.Get("/get-languages", handler.GetLanguages)
|
|
||||||
r.Get("/get-countries", handler.GetCountries)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LangsAndCountriesHandler) GetLanguages(c fiber.Ctx) error {
|
|
||||||
languages, err := h.langsAndCountriesService.GetLanguages()
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *LangsAndCountriesHandler) GetCountries(c fiber.Ctx) error {
|
|
||||||
countries, err := h.langsAndCountriesService.GetCountriesAndCurrencies()
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
package restricted
|
package restricted
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
"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/nullable"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/samber/lo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
|
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
|
||||||
@@ -41,31 +36,19 @@ func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||||
paging, filters, err := ParseProductFilters(c)
|
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingGetListing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// overrides := map[string]string{
|
id_lang, ok := c.Locals("langID").(uint)
|
||||||
// "override_country": c.Query("override_country", ""),
|
if !ok {
|
||||||
// "override_currency": c.Query("override_currency", ""),
|
|
||||||
// }
|
|
||||||
|
|
||||||
id_shop_attribute := c.Query("shopID")
|
|
||||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
listing, err := h.listProductsService.GetListing(id_lang, paging, filters)
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
listing, err := h.listProductsService.GetListing(uint(id_shop), uint(id_lang), paging, filters)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
@@ -74,62 +57,11 @@ func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
|||||||
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var columnMapping map[string]string = map[string]string{}
|
var columnMappingGetListing map[string]string = map[string]string{
|
||||||
|
"product_id": "ps.id_product",
|
||||||
// var columnMapping map[string]string = map[string]string{
|
"name": "pl.name",
|
||||||
// "product_id": "id",
|
"reference": "ps.reference",
|
||||||
// "price": "price_taxed",
|
"category_name": "cl.name",
|
||||||
// "name": "name",
|
"id_category": "cp.id_category",
|
||||||
// "category_id": "category_id",
|
"quantity": "sa.quantity",
|
||||||
// "feature_id": "feature_id",
|
|
||||||
// "feature": "feature_name",
|
|
||||||
// "value_id": "value_id",
|
|
||||||
// "value": "value_name",
|
|
||||||
// "status": "active_sale",
|
|
||||||
// "stock": "in_stock",
|
|
||||||
// }
|
|
||||||
|
|
||||||
func ParseProductFilters(c fiber.Ctx) (find.Paging, *filters.FiltersList, error) {
|
|
||||||
var p find.Paging
|
|
||||||
fl := filters.NewFiltersList()
|
|
||||||
// productFilters := new(model.ProductFilters)
|
|
||||||
|
|
||||||
// err := c.Bind().Query(productFilters)
|
|
||||||
// if err != nil {
|
|
||||||
// return p, &fl, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if productFilters.Name != "" {
|
|
||||||
// fl.Append(filters.Where("name LIKE ?", fmt.Sprintf("%%%s%%", productFilters.Name)))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if productFilters.Sort != "" {
|
|
||||||
// ord, err := query_params.ParseOrdering[model.Product](c, columnMapping)
|
|
||||||
// if err != nil {
|
|
||||||
// return p, &fl, err
|
|
||||||
// }
|
|
||||||
// for _, o := range ord {
|
|
||||||
// fl.Append(filters.Order(o.Column, o.IsDesc))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(productFilters.Features) > 0 {
|
|
||||||
// fl.Append(featureValueFilters(productFilters.Features))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fl.Append(query_params.ParseWhereScopes[model.Product](c, []string{"name"}, columnMapping)...)
|
|
||||||
|
|
||||||
pageNum, pageElems := query_params.ParsePagination(c)
|
|
||||||
p = find.Paging{Page: pageNum, Elements: pageElems}
|
|
||||||
|
|
||||||
return p, &fl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FeatVal = map[uint][]uint
|
|
||||||
|
|
||||||
func featureValueFilters(feats FeatVal) filters.Filter {
|
|
||||||
filt := func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("value_id IN ?", lo.Flatten(lo.Values(feats))).Group("id").Having("COUNT(id) = ?", len(lo.Keys(feats)))
|
|
||||||
}
|
|
||||||
return filters.NewFilter(filters.FEAT_VAL_PRODUCT_FILTER, filt)
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
52
app/delivery/web/api/restricted/localeSelector.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocaleSelectorHandler for getting languages and countries data
|
||||||
|
type LocaleSelectorHandler struct {
|
||||||
|
localeSelectorService *localeSelectorService.LocaleSelectorService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocaleSelectorHandler creates a new LocaleSelectorHandler instance
|
||||||
|
func NewLocaleSelectorHandler() *LocaleSelectorHandler {
|
||||||
|
localeSelectorService := localeSelectorService.New()
|
||||||
|
return &LocaleSelectorHandler{
|
||||||
|
localeSelectorService: localeSelectorService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewLocaleSelectorHandler()
|
||||||
|
|
||||||
|
r.Get("/get-languages", handler.GetLanguages)
|
||||||
|
r.Get("/get-countries", handler.GetCountries)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
|
||||||
|
languages, err := h.localeSelectorService.GetLanguages()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&languages, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
|
||||||
|
countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&countries, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
115
app/delivery/web/api/restricted/meiliSearch.go
Normal file
115
app/delivery/web/api/restricted/meiliSearch.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
|
||||||
|
"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 MeiliSearchHandler struct {
|
||||||
|
meiliService *meiliService.MeiliService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMeiliSearchHandler() *MeiliSearchHandler {
|
||||||
|
meiliService := meiliService.New()
|
||||||
|
return &MeiliSearchHandler{
|
||||||
|
meiliService: meiliService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewMeiliSearchHandler()
|
||||||
|
|
||||||
|
// for superadmin only
|
||||||
|
r.Get("/create-index", handler.CreateIndex)
|
||||||
|
r.Get("/test", handler.Test)
|
||||||
|
|
||||||
|
// for all users
|
||||||
|
r.Get("/search", handler.Search)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
|
||||||
|
id_lang, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.meiliService.CreateIndex(id_lang)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
nothing := ""
|
||||||
|
return c.JSON(response.Make(¬hing, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MeiliSearchHandler) Test(c fiber.Ctx) error {
|
||||||
|
id_lang, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
test, err := h.meiliService.Test(id_lang)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
|
||||||
|
id_lang, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
query := c.Query("query")
|
||||||
|
|
||||||
|
limit_attribute := c.Query("limit")
|
||||||
|
limit, err := strconv.Atoi(limit_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
id_category_attribute := c.Query("id_category")
|
||||||
|
id_category, err := strconv.Atoi(id_category_attribute)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
price_lower_bound_attribute := c.Query("price_lower_bound")
|
||||||
|
price_lower_bound, err := strconv.ParseFloat(price_lower_bound_attribute, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
price_upper_bound_attribute := c.Query("price_upper_bound")
|
||||||
|
price_upper_bound, err := strconv.ParseFloat(price_upper_bound_attribute, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
test, err := h.meiliService.Search(id_lang, query, uint(limit), uint(id_category), price_lower_bound, price_upper_bound)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package restricted
|
package restricted
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
|
"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/i18n"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
@@ -26,26 +24,33 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
|||||||
handler := NewMenuHandler()
|
handler := NewMenuHandler()
|
||||||
|
|
||||||
r.Get("/get-menu", handler.GetMenu)
|
r.Get("/get-menu", handler.GetMenu)
|
||||||
|
r.Get("/get-routes", handler.GetRouting)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
||||||
|
lang_id, ok := c.Locals("langID").(uint)
|
||||||
id_shop_attribute := c.Query("shopID")
|
if !ok {
|
||||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
menu, err := h.menuService.GetMenu(lang_id)
|
||||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
if err != nil {
|
||||||
if err != nil {
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
}
|
||||||
}
|
|
||||||
|
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
||||||
menu, err := h.menuService.GetMenu(uint(id_shop), uint(id_lang))
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) GetRouting(c fiber.Ctx) error {
|
||||||
|
lang_id, ok := c.Locals("langID").(uint)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
menu, err := h.menuService.GetRoutes(lang_id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
|||||||
@@ -54,13 +54,6 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productLangID_attribute := c.Query("productLangID")
|
productLangID_attribute := c.Query("productLangID")
|
||||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,7 +61,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID))
|
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productLangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
@@ -77,7 +70,7 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveProductDescription saves the description for a given product ID, in given shop and language
|
// SaveProductDescription saves the description for a given product ID, in given language
|
||||||
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
||||||
userID, ok := c.Locals("userID").(uint)
|
userID, ok := c.Locals("userID").(uint)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -92,13 +85,6 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productLangID_attribute := c.Query("productLangID")
|
productLangID_attribute := c.Query("productLangID")
|
||||||
productLangID, err := strconv.Atoi(productLangID_attribute)
|
productLangID, err := strconv.Atoi(productLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,7 +98,7 @@ func (h *ProductDescriptionHandler) SaveProductDescription(c fiber.Ctx) error {
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID), updates)
|
err = h.productDescriptionService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
@@ -136,13 +122,6 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
productShopID_attribute := c.Query("productShopID")
|
|
||||||
productShopID, err := strconv.Atoi(productShopID_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
productFromLangID_attribute := c.Query("productFromLangID")
|
productFromLangID_attribute := c.Query("productFromLangID")
|
||||||
productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
|
productFromLangID, err := strconv.Atoi(productFromLangID_attribute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,7 +142,7 @@ func (h *ProductDescriptionHandler) TranslateProductDescription(c fiber.Ctx) err
|
|||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
}
|
}
|
||||||
|
|
||||||
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productShopID), uint(productFromLangID), uint(productToLangID), aiModel)
|
description, err := h.productDescriptionService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ var swaggerHTML = `
|
|||||||
url: "/openapi.json",
|
url: "/openapi.json",
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
|
withCredentials: true,
|
||||||
|
"servers": [
|
||||||
|
{ "url": "http://localhost:3000" },
|
||||||
|
{ "url": "http://localhost:5173" }
|
||||||
|
],
|
||||||
presets: [
|
presets: [
|
||||||
SwaggerUIBundle.presets.apis,
|
SwaggerUIBundle.presets.apis,
|
||||||
SwaggerUIStandalonePreset
|
SwaggerUIStandalonePreset
|
||||||
|
|||||||
@@ -97,15 +97,27 @@ func (s *Server) Setup() error {
|
|||||||
listProducts := s.restricted.Group("/list-products")
|
listProducts := s.restricted.Group("/list-products")
|
||||||
restricted.ListProductsHandlerRoutes(listProducts)
|
restricted.ListProductsHandlerRoutes(listProducts)
|
||||||
|
|
||||||
// changing the JWT cookies routes (restricted)
|
// locale selector (restricted)
|
||||||
// in reality it just handles changing user's country and language
|
// this is basically for changing user's selected language and country
|
||||||
langsAndCountries := s.restricted.Group("/langs-and-countries")
|
localeSelector := s.restricted.Group("/langs-and-countries")
|
||||||
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries)
|
restricted.LocaleSelectorHandlerRoutes(localeSelector)
|
||||||
|
|
||||||
// menu (restricted)
|
// menu (restricted)
|
||||||
menu := s.restricted.Group("/menu")
|
menu := s.restricted.Group("/menu")
|
||||||
restricted.MenuHandlerRoutes(menu)
|
restricted.MenuHandlerRoutes(menu)
|
||||||
|
|
||||||
|
// meili search (restricted)
|
||||||
|
meiliSearch := s.restricted.Group("/meili-search")
|
||||||
|
restricted.MeiliSearchHandlerRoutes(meiliSearch)
|
||||||
|
|
||||||
|
// carts (restricted)
|
||||||
|
carts := s.restricted.Group("/carts")
|
||||||
|
restricted.CartsHandlerRoutes(carts)
|
||||||
|
|
||||||
|
s.api.All("*", func(c fiber.Ctx) error {
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
// // Restricted routes example
|
// // Restricted routes example
|
||||||
// restricted := s.api.Group("/restricted")
|
// restricted := s.api.Group("/restricted")
|
||||||
// restricted.Use(middleware.AuthMiddleware())
|
// restricted.Use(middleware.AuthMiddleware())
|
||||||
|
|||||||
24
app/model/cart.go
Normal file
24
app/model/cart.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type CustomerCart struct {
|
||||||
|
CartID uint `gorm:"column:cart_id;primaryKey;autoIncrement" json:"cart_id"`
|
||||||
|
UserID uint `gorm:"column:user_id;not null;index" json:"-"`
|
||||||
|
Name *string `gorm:"column:name;size:255" json:"name,omitempty"`
|
||||||
|
Products []CartProduct `gorm:"foreignKey:CartID;references:CartID" json:"products,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CustomerCart) TableName() string {
|
||||||
|
return "b2b_customer_carts"
|
||||||
|
}
|
||||||
|
|
||||||
|
type CartProduct struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"-"`
|
||||||
|
CartID uint `gorm:"column:cart_id;not null;index" json:"-"`
|
||||||
|
ProductID uint `gorm:"column:product_id;not null" json:"product_id"`
|
||||||
|
ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"`
|
||||||
|
Amount uint `gorm:"column:amount;not null" json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CartProduct) TableName() string {
|
||||||
|
return "b2b_carts_products"
|
||||||
|
}
|
||||||
@@ -9,3 +9,16 @@ type Country struct {
|
|||||||
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"`
|
CurrencyISOCode string `gorm:"column:iso_code" json:"currency_iso_code"`
|
||||||
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
CurrencyName string `gorm:"column:name" json:"currency_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Country) TableName() string {
|
||||||
|
return "b2b_countries"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PSCountry struct {
|
||||||
|
ID uint `gorm:"primaryKey;column:id_country" json:"id"`
|
||||||
|
CurrencyID uint `gorm:"column:id_currency" json:"currency_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PSCountry) TableName() string {
|
||||||
|
return "ps_country"
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,11 +62,14 @@ type Product struct {
|
|||||||
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
||||||
}
|
}
|
||||||
type ProductInList struct {
|
type ProductInList struct {
|
||||||
ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
|
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
|
||||||
Name string `gorm:"column:name" json:"name" form:"name"`
|
Name string `gorm:"column:name" json:"name" form:"name"`
|
||||||
ImageID uint `gorm:"column:id_image"`
|
LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"`
|
||||||
LinkRewrite string `gorm:"column:link_rewrite"`
|
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
||||||
Active uint `gorm:"column:active" json:"active" form:"active"`
|
CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"`
|
||||||
|
Reference string `gorm:"column:reference" json:"reference"`
|
||||||
|
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
|
||||||
|
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductFilters struct {
|
type ProductFilters struct {
|
||||||
@@ -82,18 +85,27 @@ type ProductFilters struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ScannedCategory struct {
|
type ScannedCategory struct {
|
||||||
CategoryID uint `gorm:"column:ID;primaryKey"`
|
CategoryID uint `gorm:"column:ID;primaryKey"`
|
||||||
Name string `gorm:"column:name"`
|
Name string `gorm:"column:name"`
|
||||||
Active uint `gorm:"column:active"`
|
Active uint `gorm:"column:active"`
|
||||||
Position uint `gorm:"column:position"`
|
Position uint `gorm:"column:position"`
|
||||||
ParentID uint `gorm:"column:id_parent"`
|
ParentID uint `gorm:"column:id_parent"`
|
||||||
IsRoot uint `gorm:"column:is_root_category"`
|
IsRoot uint `gorm:"column:is_root_category"`
|
||||||
|
LinkRewrite string `gorm:"column:link_rewrite"`
|
||||||
|
IsoCode string `gorm:"column:iso_code"`
|
||||||
}
|
}
|
||||||
type Category struct {
|
type Category struct {
|
||||||
CategoryID uint `json:"category_id" form:"category_id"`
|
CategoryID uint `json:"category_id" form:"category_id"`
|
||||||
Name string `json:"name" form:"name"`
|
Label string `json:"label" form:"label"`
|
||||||
Active uint `json:"active" form:"active"`
|
// Active bool `json:"active" form:"active"`
|
||||||
Subcategories []Category `json:"subcategories" form:"subcategories"`
|
Params CategpryParams `json:"params" form:"params"`
|
||||||
|
Children []Category `json:"children" form:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategpryParams struct {
|
||||||
|
CategoryID uint `json:"category_id" form:"category_id"`
|
||||||
|
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
|
||||||
|
Locale string `json:"locale" form:"locale"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatVal = map[uint][]uint
|
type FeatVal = map[uint][]uint
|
||||||
|
|||||||
@@ -18,3 +18,30 @@ type ProductDescription struct {
|
|||||||
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
||||||
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductRow struct {
|
||||||
|
IDProduct int `gorm:"column:id_product"`
|
||||||
|
IDShop int `gorm:"column:id_shop"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Active uint8 `gorm:"column:active"`
|
||||||
|
Reference string `gorm:"column:reference"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeiliSearchProduct struct {
|
||||||
|
ProductID uint `gorm:"column:id_product"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Active uint8 `gorm:"column:active"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
Description string `gorm:"column:description"`
|
||||||
|
DescriptionShort string `gorm:"column:description_short"`
|
||||||
|
Usage string `gorm:"column:used_for"`
|
||||||
|
EAN13 string `gorm:"column:ean13"`
|
||||||
|
Reference string `gorm:"column:reference"`
|
||||||
|
Width float64 `gorm:"column:width"`
|
||||||
|
Height float64 `gorm:"column:height"`
|
||||||
|
Depth float64 `gorm:"column:depth"`
|
||||||
|
Weight float64 `gorm:"column:weight"`
|
||||||
|
CategoryID uint `gorm:"column:id_category"`
|
||||||
|
CategoryName string `gorm:"column:category_name"`
|
||||||
|
Variations uint `gorm:"column:variations"`
|
||||||
|
}
|
||||||
|
|||||||
21
app/model/routing.go
Normal file
21
app/model/routing.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `gorm:"type:varchar(255);not null;unique"`
|
||||||
|
Path *string `gorm:"type:varchar(255);default:null"`
|
||||||
|
Component string `gorm:"type:varchar(255);not null;comment:path to component file"`
|
||||||
|
Layout *string `gorm:"type:varchar(50);default:'default';comment:'default | empty'"`
|
||||||
|
Meta *string `gorm:"type:longtext;default:'{}'"`
|
||||||
|
IsActive *bool `gorm:"type:tinyint;default:1"`
|
||||||
|
SortOrder *int `gorm:"type:int;default:0"`
|
||||||
|
|
||||||
|
ParentID *uint `gorm:"index"`
|
||||||
|
Parent *Route `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:SET NULL;foreignKey:ParentID"`
|
||||||
|
|
||||||
|
Children []Route `gorm:"foreignKey:ParentID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Route) TableName() string {
|
||||||
|
return "b2b_routes"
|
||||||
|
}
|
||||||
133
app/repos/cartsRepo/cartsRepo.go
Normal file
133
app/repos/cartsRepo/cartsRepo.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package cartsRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UICartsRepo interface {
|
||||||
|
CartsAmount(user_id uint) (uint, error)
|
||||||
|
CreateNewCart(user_id uint) (model.CustomerCart, error)
|
||||||
|
UserHasCart(user_id uint, cart_id uint) (uint, error)
|
||||||
|
UpdateCartName(user_id uint, cart_id uint, new_name string) error
|
||||||
|
RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error)
|
||||||
|
RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error)
|
||||||
|
CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error)
|
||||||
|
AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CartsRepo struct{}
|
||||||
|
|
||||||
|
func New() UICartsRepo {
|
||||||
|
return &CartsRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) CartsAmount(user_id uint) (uint, error) {
|
||||||
|
var amt uint
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("b2b_customer_carts").
|
||||||
|
Select("COUNT(*) AS amt").
|
||||||
|
Where("user_id = ?", user_id).
|
||||||
|
Scan(&amt).
|
||||||
|
Error
|
||||||
|
|
||||||
|
return amt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) {
|
||||||
|
var name string
|
||||||
|
name = constdata.DEFAULT_NEW_CART_NAME
|
||||||
|
|
||||||
|
cart := model.CustomerCart{
|
||||||
|
UserID: user_id,
|
||||||
|
Name: &name,
|
||||||
|
}
|
||||||
|
err := db.DB.Create(&cart).Error
|
||||||
|
|
||||||
|
return cart, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) {
|
||||||
|
var amt uint
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("b2b_customer_carts").
|
||||||
|
Select("COUNT(*) AS amt").
|
||||||
|
Where("user_id = ? AND cart_id = ?", user_id, cart_id).
|
||||||
|
Scan(&amt).
|
||||||
|
Error
|
||||||
|
|
||||||
|
return amt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
|
||||||
|
err := db.DB.
|
||||||
|
Table("b2b_customer_carts").
|
||||||
|
Where("user_id = ? AND cart_id = ?", user_id, cart_id).
|
||||||
|
Update("name", new_name).
|
||||||
|
Error
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) {
|
||||||
|
var carts []model.CustomerCart
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("b2b_customer_carts").
|
||||||
|
Where("user_id = ?", user_id).
|
||||||
|
Scan(&carts).
|
||||||
|
Error
|
||||||
|
|
||||||
|
return carts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) {
|
||||||
|
var cart model.CustomerCart
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Preload("Products").
|
||||||
|
Where("user_id = ? AND cart_id = ?", user_id, cart_id).
|
||||||
|
First(&cart).
|
||||||
|
Error
|
||||||
|
|
||||||
|
return &cart, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) {
|
||||||
|
var amt uint
|
||||||
|
|
||||||
|
if product_attribute_id == nil {
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_shop").
|
||||||
|
Select("COUNT(*) AS amt").
|
||||||
|
Where("id_product = ?", product_id).
|
||||||
|
Scan(&amt).
|
||||||
|
Error
|
||||||
|
return amt, err
|
||||||
|
|
||||||
|
} else {
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_shop AS ps").
|
||||||
|
Joins("INNER JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product").
|
||||||
|
Select("COUNT(*) AS amt").
|
||||||
|
Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id).
|
||||||
|
Scan(&amt).
|
||||||
|
Error
|
||||||
|
return amt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CartsRepo) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error {
|
||||||
|
product := model.CartProduct{
|
||||||
|
CartID: cart_id,
|
||||||
|
ProductID: product_id,
|
||||||
|
ProductAttributeID: product_attribute_id,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
err := db.DB.Create(&product).Error
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
52
app/repos/categoriesRepo/categoriesRepo.go
Normal file
52
app/repos/categoriesRepo/categoriesRepo.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package categoriesRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UICategoriesRepo interface {
|
||||||
|
GetAllCategories(id_lang uint) ([]model.ScannedCategory, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoriesRepo struct{}
|
||||||
|
|
||||||
|
func New() UICategoriesRepo {
|
||||||
|
return &CategoriesRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) {
|
||||||
|
var allCategories []model.ScannedCategory
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_category").
|
||||||
|
Select(`
|
||||||
|
ps_category.id_category AS id,
|
||||||
|
ps_category_lang.name AS name,
|
||||||
|
ps_category.active AS active,
|
||||||
|
ps_category_shop.position AS position,
|
||||||
|
ps_category.id_parent AS id_parent,
|
||||||
|
ps_category.is_root_category AS is_root_category,
|
||||||
|
ps_category_lang.link_rewrite AS link_rewrite,
|
||||||
|
ps_lang.iso_code AS iso_code
|
||||||
|
`).
|
||||||
|
Joins(`
|
||||||
|
LEFT JOIN ps_category_lang
|
||||||
|
ON ps_category_lang.id_category = ps_category.id_category
|
||||||
|
AND ps_category_lang.id_shop = ?
|
||||||
|
AND ps_category_lang.id_lang = ?
|
||||||
|
`, constdata.SHOP_ID, id_lang).
|
||||||
|
Joins(`
|
||||||
|
LEFT JOIN ps_category_shop
|
||||||
|
ON ps_category_shop.id_category = ps_category.id_category
|
||||||
|
AND ps_category_shop.id_shop = ?
|
||||||
|
`, constdata.SHOP_ID).
|
||||||
|
Joins(`
|
||||||
|
JOIN ps_lang
|
||||||
|
ON ps_lang.id_lang = ps_category_lang.id_lang
|
||||||
|
`).
|
||||||
|
Scan(&allCategories).Error
|
||||||
|
|
||||||
|
return allCategories, err
|
||||||
|
}
|
||||||
70
app/repos/listProductsRepo/listProductsRepo.go
Normal file
70
app/repos/listProductsRepo/listProductsRepo.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package listProductsRepo
|
||||||
|
|
||||||
|
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 UIListProductsRepo interface {
|
||||||
|
GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProductsRepo struct{}
|
||||||
|
|
||||||
|
func New() UIListProductsRepo {
|
||||||
|
return &ListProductsRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||||
|
var listing []model.ProductInList
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := db.Get().
|
||||||
|
Table("ps_product_shop AS ps").
|
||||||
|
Select(`
|
||||||
|
ps.id_product AS product_id,
|
||||||
|
pl.name AS name,
|
||||||
|
pl.link_rewrite AS link_rewrite,
|
||||||
|
CONCAT('https://www.naluconcept.com', '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
||||||
|
cl.name AS category_name,
|
||||||
|
p.reference AS reference,
|
||||||
|
COUNT(DISTINCT pas.id_product_attribute) AS variants_number,
|
||||||
|
sa.quantity AS quantity
|
||||||
|
`).
|
||||||
|
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
|
||||||
|
Joins("JOIN ps_category_product cp ON ps.id_product = cp.id_product").
|
||||||
|
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang).
|
||||||
|
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
|
||||||
|
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang).
|
||||||
|
Joins("LEFT JOIN ps_product_attribute_shop pas ON pas.id_product = cp.id_product").
|
||||||
|
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product").
|
||||||
|
Where("ps.active = ?", 1).
|
||||||
|
Group("cp.id_product")
|
||||||
|
|
||||||
|
// Apply all filters
|
||||||
|
if filt != nil {
|
||||||
|
filt.ApplyAll(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run counter first as query is without limit and offset
|
||||||
|
err := query.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.ProductInList]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.
|
||||||
|
Order("ps.id_product DESC").
|
||||||
|
Limit(p.Limit()).
|
||||||
|
Offset(p.Offset()).
|
||||||
|
Scan(&listing).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.ProductInList]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return find.Found[model.ProductInList]{
|
||||||
|
Items: listing,
|
||||||
|
Count: uint(total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
36
app/repos/localeSelectorRepo/localeSelectorRepo.go
Normal file
36
app/repos/localeSelectorRepo/localeSelectorRepo.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package localeSelectorRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UILocaleSelectorRepo interface {
|
||||||
|
GetLanguages() ([]model.Language, error)
|
||||||
|
GetCountriesAndCurrencies() ([]model.Country, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocaleSelectorRepo struct{}
|
||||||
|
|
||||||
|
func New() UILocaleSelectorRepo {
|
||||||
|
return &LocaleSelectorRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *LocaleSelectorRepo) GetLanguages() ([]model.Language, error) {
|
||||||
|
var languages []model.Language
|
||||||
|
|
||||||
|
err := db.DB.Table("b2b_language").Scan(&languages).Error
|
||||||
|
|
||||||
|
return languages, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *LocaleSelectorRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||||
|
var countries []model.Country
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Select("b2b_countries.id, b2b_countries.name, b2b_countries.flag, ps_currency.id_currency as id_currency, ps_currency.name as currency_name, ps_currency.iso_code as currency_iso_code").
|
||||||
|
Joins("JOIN ps_currency ON ps_currency.id_currency = b2b_countries.currency").
|
||||||
|
Scan(&countries).Error
|
||||||
|
|
||||||
|
return countries, err
|
||||||
|
}
|
||||||
114
app/repos/productDescriptionRepo/productDescriptionRepo.go
Normal file
114
app/repos/productDescriptionRepo/productDescriptionRepo.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package productDescriptionRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UIProductDescriptionRepo interface {
|
||||||
|
GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error)
|
||||||
|
CreateIfDoesNotExist(productID uint, productLangID uint) error
|
||||||
|
UpdateFields(productID uint, productLangID uint, updates map[string]string) error
|
||||||
|
GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductDescriptionRepo struct{}
|
||||||
|
|
||||||
|
func New() UIProductDescriptionRepo {
|
||||||
|
return &ProductDescriptionRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that any user has access to all product descriptions
|
||||||
|
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||||
|
var ProductDescription model.ProductDescription
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
|
First(&ProductDescription).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProductDescription, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't exist, returns an error.
|
||||||
|
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productLangID uint) error {
|
||||||
|
record := model.ProductDescription{
|
||||||
|
ProductID: productID,
|
||||||
|
ShopID: constdata.SHOP_ID,
|
||||||
|
LangID: productLangID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
|
FirstOrCreate(&record).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productLangID uint, updates map[string]string) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updatesIface := make(map[string]interface{}, len(updates))
|
||||||
|
for k, v := range updates {
|
||||||
|
updatesIface[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Table("ps_product_lang").
|
||||||
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, constdata.SHOP_ID, productLangID).
|
||||||
|
Updates(updatesIface).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that any user has access to all product descriptions
|
||||||
|
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSearchProduct, error) {
|
||||||
|
var products []model.MeiliSearchProduct
|
||||||
|
|
||||||
|
err := db.DB.
|
||||||
|
Select(`
|
||||||
|
ps.id_product AS id_product,
|
||||||
|
pl.name AS name,
|
||||||
|
ps.active AS active,
|
||||||
|
ps.price AS price,
|
||||||
|
pl.description AS description,
|
||||||
|
pl.description_short AS description_short,
|
||||||
|
pl.usage AS used_for,
|
||||||
|
p.ean13 AS ean13,
|
||||||
|
p.reference AS reference,
|
||||||
|
p.width AS width,
|
||||||
|
p.height AS height,
|
||||||
|
p.depth AS depth,
|
||||||
|
p.weight AS weight,
|
||||||
|
ps.id_category_default AS id_category,
|
||||||
|
cl.name AS category_name,
|
||||||
|
COUNT(DISTINCT pas.id_product_attribute) AS variations
|
||||||
|
`).
|
||||||
|
Table("ps_product_shop AS ps").
|
||||||
|
Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||||
|
Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product").
|
||||||
|
Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
|
||||||
|
Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID).
|
||||||
|
Where("ps.id_shop = ?", constdata.SHOP_ID).
|
||||||
|
Group("ps.id_product").
|
||||||
|
Scan(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
return products, fmt.Errorf("database error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
25
app/repos/routesRepo/routesRepo.go
Normal file
25
app/repos/routesRepo/routesRepo.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package routesrepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UIRoutesRepo interface {
|
||||||
|
GetRoutes(langId uint) ([]model.Route, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoutesRepo struct{}
|
||||||
|
|
||||||
|
func New() UIRoutesRepo {
|
||||||
|
return &RoutesRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
|
||||||
|
routes := []model.Route{}
|
||||||
|
err := db.DB.Find(&routes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
83
app/service/cartsService/cartsService.go
Normal file
83
app/service/cartsService/cartsService.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package cartsService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CartsService struct {
|
||||||
|
repo cartsRepo.UICartsRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *CartsService {
|
||||||
|
return &CartsService{
|
||||||
|
repo: cartsRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) {
|
||||||
|
var cart model.CustomerCart
|
||||||
|
|
||||||
|
customers_carts_amount, err := s.repo.CartsAmount(user_id)
|
||||||
|
if err != nil {
|
||||||
|
return cart, err
|
||||||
|
}
|
||||||
|
if customers_carts_amount >= constdata.MAX_AMOUNT_OF_CARTS_PER_USER {
|
||||||
|
return cart, responseErrors.ErrMaxAmtOfCartsReached
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new cart for customer
|
||||||
|
cart, err = s.repo.CreateNewCart(user_id)
|
||||||
|
|
||||||
|
return cart, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
|
||||||
|
amt, err := s.repo.UserHasCart(user_id, cart_id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if amt != 1 {
|
||||||
|
return responseErrors.ErrUserHasNoSuchCart
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.UpdateCartName(user_id, cart_id, new_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CartsService) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) {
|
||||||
|
return s.repo.RetrieveCartsInfo(user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) {
|
||||||
|
amt, err := s.repo.UserHasCart(user_id, cart_id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if amt != 1 {
|
||||||
|
return nil, responseErrors.ErrUserHasNoSuchCart
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.RetrieveCart(user_id, cart_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error {
|
||||||
|
amt, err := s.repo.UserHasCart(user_id, cart_id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if amt != 1 {
|
||||||
|
return responseErrors.ErrUserHasNoSuchCart
|
||||||
|
}
|
||||||
|
|
||||||
|
amt, err = s.repo.CheckProductExists(product_id, product_attribute_id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if amt != 1 {
|
||||||
|
return responseErrors.ErrProductOrItsVariationDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.AddProduct(user_id, cart_id, product_id, product_attribute_id, amount)
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package langsAndCountriesService
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/repository/langsAndCountriesRepo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LangsAndCountriesService literally sends back language and countries information.
|
|
||||||
type LangsAndCountriesService struct {
|
|
||||||
repo langsAndCountriesRepo.UILangsAndCountriesRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLangsAndCountriesService creates a new LangsAndCountries service
|
|
||||||
func New() *LangsAndCountriesService {
|
|
||||||
return &LangsAndCountriesService{
|
|
||||||
repo: langsAndCountriesRepo.New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LangsAndCountriesService) GetLanguages() ([]model.Language, error) {
|
|
||||||
return s.repo.GetLanguages()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LangsAndCountriesService) GetCountriesAndCurrencies() ([]model.Country, error) {
|
|
||||||
return s.repo.GetCountriesAndCurrencies()
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package langsService
|
package langsService
|
||||||
|
|
||||||
import (
|
import (
|
||||||
langs_repo "git.ma-al.com/goc_daniel/b2b/app/langs"
|
langs_repo "git.ma-al.com/goc_daniel/b2b/app/repos/langsRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
"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/response"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package listProductsService
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/listProductsRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
"git.ma-al.com/goc_daniel/b2b/repository/listProductsRepo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListProductsService struct {
|
type ListProductsService struct {
|
||||||
@@ -17,43 +17,13 @@ func New() *ListProductsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ListProductsService) GetListing(id_shop uint, id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
func (s *ListProductsService) GetListing(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
||||||
var products find.Found[model.ProductInList]
|
var products find.Found[model.ProductInList]
|
||||||
|
|
||||||
// currencyIso := c.Cookies("currency_iso", "")
|
products, err := s.listProductsRepo.GetListing(id_lang, p, filters)
|
||||||
// countryIso := c.Cookies("country_iso", "")
|
|
||||||
|
|
||||||
// if overrides["override_currency"] != "" {
|
|
||||||
// currencyIso = overrides["override_currency"]
|
|
||||||
// }
|
|
||||||
// if overrides["override_country"] != "" {
|
|
||||||
// countryIso = overrides["override_country"]
|
|
||||||
// }
|
|
||||||
|
|
||||||
products, err := s.listProductsRepo.GetListing(id_shop, id_lang, p, filters)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return products, err
|
return products, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// var loopErr error
|
|
||||||
// parallel.ForEach(products.Items, func(t model.Product, i int) {
|
|
||||||
// // products.Items[i].PriceTaxed *= currRate.Rate.InexactFloat64()
|
|
||||||
// // products.Items[i].PriceTaxed = tiny_util.RoundUpMonetary(products.Items[i].PriceTaxed)
|
|
||||||
|
|
||||||
// if products.Items[i].Name.IsNull() {
|
|
||||||
// translation, err := s.listProductsRepo.GetTranslation(ctx, products.Items[i].ID, defaults.DefaultLanguageID)
|
|
||||||
// if err != nil {
|
|
||||||
// loopErr = err
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// products.Items[i].Name = nullable.FromPrimitiveString(translation.Name)
|
|
||||||
// products.Items[i].DescriptionShort = nullable.FromPrimitiveString(translation.DescriptionShort)
|
|
||||||
// products.Items[i].LinkRewrite = nullable.FromPrimitiveString(translation.LinkRewrite)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// if loopErr != nil {
|
|
||||||
// return products, errs.Handled(span, loopErr, errs.InternalError, errs.ERR_TODO)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return products, nil
|
return products, nil
|
||||||
}
|
}
|
||||||
|
|||||||
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
26
app/service/localeSelectorService/localeSelectorService.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package localeSelectorService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/localeSelectorRepo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocaleSelectorService literally sends back language and countries information.
|
||||||
|
type LocaleSelectorService struct {
|
||||||
|
repo localeSelectorRepo.UILocaleSelectorRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocaleSelectorService creates a new LocaleSelector service
|
||||||
|
func New() *LocaleSelectorService {
|
||||||
|
return &LocaleSelectorService{
|
||||||
|
repo: localeSelectorRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocaleSelectorService) GetLanguages() ([]model.Language, error) {
|
||||||
|
return s.repo.GetLanguages()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LocaleSelectorService) GetCountriesAndCurrencies() ([]model.Country, error) {
|
||||||
|
return s.repo.GetCountriesAndCurrencies()
|
||||||
|
}
|
||||||
8
app/service/meiliService/blank.json
Normal file
8
app/service/meiliService/blank.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"products-openai": {
|
||||||
|
"source": "openAi",
|
||||||
|
"model": "text-embedding-3-small",
|
||||||
|
"apiKey": "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",
|
||||||
|
"documentTemplate": "{{doc.Name}} is equipment used for {{doc.Description | truncatewords: 20}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
240
app/service/meiliService/meiliService.go
Normal file
240
app/service/meiliService/meiliService.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package meiliService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
|
||||||
|
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
|
||||||
|
"github.com/meilisearch/meilisearch-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeiliService struct {
|
||||||
|
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
|
||||||
|
meiliClient meilisearch.ServiceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *MeiliService {
|
||||||
|
meiliURL := os.Getenv("MEILISEARCH_URL")
|
||||||
|
meiliAPIKey := os.Getenv("MEILISEARCH_API_KEY")
|
||||||
|
|
||||||
|
client := meilisearch.New(
|
||||||
|
meiliURL,
|
||||||
|
meilisearch.WithAPIKey(meiliAPIKey),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &MeiliService{
|
||||||
|
meiliClient: client,
|
||||||
|
productDescriptionRepo: productDescriptionRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================== FOR SUPERADMIN ONLY ====================================
|
||||||
|
func (s *MeiliService) CreateIndex(id_lang uint) error {
|
||||||
|
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||||
|
|
||||||
|
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
|
||||||
|
for i := 0; i < len(products); i++ {
|
||||||
|
products[i].Description, err = cleanHTML(products[i].Description)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].Description: %v\n", products[i].Description)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at description")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
products[i].DescriptionShort, err = cleanHTML(products[i].DescriptionShort)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].DescriptionShort: %v\n", products[i].DescriptionShort)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at description short")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
products[i].Usage, err = cleanHTML(products[i].Usage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("products[i].Usage: %v\n", products[i].Usage)
|
||||||
|
fmt.Printf("products[i].ProductID: %v\n", products[i].ProductID)
|
||||||
|
fmt.Println("failed at usage")
|
||||||
|
fmt.Printf("err: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryKey := "ProductID"
|
||||||
|
docOptions := &meilisearch.DocumentOptions{
|
||||||
|
PrimaryKey: &primaryKey,
|
||||||
|
SkipCreation: false,
|
||||||
|
}
|
||||||
|
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||||
|
}
|
||||||
|
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||||
|
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||||
|
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||||
|
|
||||||
|
filterableAttributes := []interface{}{
|
||||||
|
"CategoryID",
|
||||||
|
"Price",
|
||||||
|
}
|
||||||
|
task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||||
|
}
|
||||||
|
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||||
|
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||||
|
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||||
|
|
||||||
|
displayedAttributes := []string{
|
||||||
|
"ProductID",
|
||||||
|
"Name",
|
||||||
|
"Active",
|
||||||
|
"Price",
|
||||||
|
"EAN13",
|
||||||
|
"Reference",
|
||||||
|
"Variations",
|
||||||
|
}
|
||||||
|
task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||||
|
}
|
||||||
|
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||||
|
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||||
|
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||||
|
|
||||||
|
searchableAttributes := []string{
|
||||||
|
"Name",
|
||||||
|
"DescriptionShort",
|
||||||
|
"Reference",
|
||||||
|
"EAN13",
|
||||||
|
"CategoryName",
|
||||||
|
"Description",
|
||||||
|
"Usage",
|
||||||
|
}
|
||||||
|
task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("meili AddDocuments error: %w", err)
|
||||||
|
}
|
||||||
|
finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
|
||||||
|
fmt.Printf("Task status: %s\n", finishedTask.Status)
|
||||||
|
fmt.Printf("Task error: %s\n", finishedTask.Error)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================== FOR DEBUG ONLY ====================================
|
||||||
|
func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) {
|
||||||
|
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||||
|
|
||||||
|
searchReq := &meilisearch.SearchRequest{
|
||||||
|
Limit: 4,
|
||||||
|
Facets: []string{
|
||||||
|
"CategoryID",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
results, err := s.meiliClient.Index(indexName).Search("mat", searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Meilisearch error: %v\n", err)
|
||||||
|
return meilisearch.SearchResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Search results for query 'mat' in %s: %d hits\n", indexName, len(results.Hits))
|
||||||
|
|
||||||
|
return *results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search performs a full-text search on the specified index
|
||||||
|
func (s *MeiliService) Search(id_lang uint, query string, limit uint, id_category uint, price_lower_bound float64, price_upper_bound float64) (meilisearch.SearchResponse, error) {
|
||||||
|
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
|
||||||
|
|
||||||
|
filter_query := ""
|
||||||
|
if id_category != 0 {
|
||||||
|
filter_query = "CategoryID = " + strconv.FormatUint(uint64(id_category), 10)
|
||||||
|
}
|
||||||
|
if price_lower_bound > 0.0 {
|
||||||
|
if filter_query != "" {
|
||||||
|
filter_query += " AND "
|
||||||
|
}
|
||||||
|
filter_query += "Price >= " + strconv.FormatFloat(price_lower_bound, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
if price_upper_bound > 0.0 {
|
||||||
|
if filter_query != "" {
|
||||||
|
filter_query += " AND "
|
||||||
|
}
|
||||||
|
filter_query += "Price <= " + strconv.FormatFloat(price_upper_bound, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq := &meilisearch.SearchRequest{
|
||||||
|
Limit: int64(limit),
|
||||||
|
Facets: []string{
|
||||||
|
"CategoryID",
|
||||||
|
},
|
||||||
|
Filter: filter_query,
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.meiliClient.Index(indexName).Search(query, searchReq)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Meilisearch search error: %v\n", err)
|
||||||
|
return meilisearch.SearchResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck checks if Meilisearch is healthy and accessible
|
||||||
|
func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
|
||||||
|
health, err := s.meiliClient.Health()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("meilisearch health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return health, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all tags from HTML text
|
||||||
|
func cleanHTML(s string) (string, error) {
|
||||||
|
r := strings.NewReader(s)
|
||||||
|
d := xml.NewDecoder(r)
|
||||||
|
|
||||||
|
text := ""
|
||||||
|
|
||||||
|
// Configure the decoder for HTML; leave off strict and autoclose for XHTML
|
||||||
|
d.Strict = true
|
||||||
|
d.AutoClose = xml.HTMLAutoClose
|
||||||
|
d.Entity = xml.HTMLEntity
|
||||||
|
for {
|
||||||
|
token, err := d.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return text, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := token.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if len(text) > 0 && text[len(text)-1] != '\n' {
|
||||||
|
text += " \n "
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
case xml.CharData:
|
||||||
|
if strings.TrimSpace(string(v)) != "" {
|
||||||
|
text += string(v)
|
||||||
|
}
|
||||||
|
case xml.Comment:
|
||||||
|
case xml.ProcInst:
|
||||||
|
case xml.Directive:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
@@ -4,24 +4,27 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo"
|
||||||
|
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
"git.ma-al.com/goc_daniel/b2b/repository/categoriesRepo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MenuService struct {
|
type MenuService struct {
|
||||||
categoriesRepo categoriesRepo.UICategoriesRepo
|
categoriesRepo categoriesRepo.UICategoriesRepo
|
||||||
|
routesRepo routesRepo.UIRoutesRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *MenuService {
|
func New() *MenuService {
|
||||||
return &MenuService{
|
return &MenuService{
|
||||||
categoriesRepo: categoriesRepo.New(),
|
categoriesRepo: categoriesRepo.New(),
|
||||||
|
routesRepo: routesRepo.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error) {
|
func (s *MenuService) GetMenu(id_lang uint) (*model.Category, error) {
|
||||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_shop, id_lang)
|
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Category{}, err
|
return &model.Category{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the root
|
// find the root
|
||||||
@@ -35,7 +38,7 @@ func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !root_found {
|
if !root_found {
|
||||||
return model.Category{}, responseErrors.ErrNoRootFound
|
return &model.Category{}, responseErrors.ErrNoRootFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// now create the children and reorder them according to position
|
// now create the children and reorder them according to position
|
||||||
@@ -57,25 +60,31 @@ func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error
|
|||||||
// finally, create the tree
|
// finally, create the tree
|
||||||
tree := s.createTree(root_index, &all_categories, &children_indices)
|
tree := s.createTree(root_index, &all_categories, &children_indices)
|
||||||
|
|
||||||
return tree, nil
|
return &tree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
|
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
|
||||||
node := s.scannedToNormalCategory((*all_categories)[index])
|
node := s.scannedToNormalCategory((*all_categories)[index])
|
||||||
|
|
||||||
for i := 0; i < len((*children_indices)[index]); i++ {
|
for i := 0; i < len((*children_indices)[index]); i++ {
|
||||||
node.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
|
node.Children = append(node.Children, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MenuService) GetRoutes(id_lang uint) ([]model.Route, error) {
|
||||||
|
return s.routesRepo.GetRoutes(id_lang)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
|
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
|
||||||
var normal model.Category
|
var normal model.Category
|
||||||
normal.Active = scanned.Active
|
// normal.Active = scanned.Active
|
||||||
normal.CategoryID = scanned.CategoryID
|
normal.CategoryID = scanned.CategoryID
|
||||||
normal.Name = scanned.Name
|
normal.Label = scanned.Name
|
||||||
normal.Subcategories = []model.Category{}
|
// normal.Active = scanned.Active == 1
|
||||||
|
normal.Params = model.CategpryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode}
|
||||||
|
normal.Children = []model.Category{}
|
||||||
return normal
|
return normal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
"cloud.google.com/go/translate/apiv3/translatepb"
|
"cloud.google.com/go/translate/apiv3/translatepb"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"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/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
"git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
|
|
||||||
"github.com/openai/openai-go/v3"
|
"github.com/openai/openai-go/v3"
|
||||||
"github.com/openai/openai-go/v3/option"
|
"github.com/openai/openai-go/v3/option"
|
||||||
"github.com/openai/openai-go/v3/responses"
|
"github.com/openai/openai-go/v3/responses"
|
||||||
@@ -70,7 +70,7 @@ func New() *ProductDescriptionService {
|
|||||||
log.Fatalf("productDescriptionService: cannot create Translation client: %v", err)
|
log.Fatalf("productDescriptionService: cannot create Translation client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
openAIClient := openai.NewClient(option.WithAPIKey("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"),
|
openAIClient := openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_KEY")),
|
||||||
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
||||||
|
|
||||||
return &ProductDescriptionService{
|
return &ProductDescriptionService{
|
||||||
@@ -82,12 +82,12 @@ func New() *ProductDescriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productLangID uint) (*model.ProductDescription, error) {
|
||||||
return s.productDescriptionRepo.GetProductDescription(productID, productShopID, productLangID)
|
return s.productDescriptionRepo.GetProductDescription(productID, productLangID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates relevant fields with the "updates" map
|
// Updates relevant fields with the "updates" map
|
||||||
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error {
|
||||||
// only some fields can be affected
|
// only some fields can be affected
|
||||||
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
||||||
for key := range updates {
|
for key := range updates {
|
||||||
@@ -106,12 +106,12 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productShopID, productLangID)
|
err := s.productDescriptionRepo.CreateIfDoesNotExist(productID, productLangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.productDescriptionRepo.UpdateFields(productID, productShopID, productLangID, updates)
|
return s.productDescriptionRepo.UpdateFields(productID, productLangID, updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateProductDescription fetches the product description for productFromLangID,
|
// TranslateProductDescription fetches the product description for productFromLangID,
|
||||||
@@ -120,9 +120,9 @@ func (s *ProductDescriptionService) SaveProductDescription(userID uint, productI
|
|||||||
//
|
//
|
||||||
// The Google Cloud project must have the Cloud Translation API enabled and the
|
// The Google Cloud project must have the Cloud Translation API enabled and the
|
||||||
// service account must hold the "Cloud Translation API User" role.
|
// service account must hold the "Cloud Translation API User" role.
|
||||||
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productFromLangID uint, productToLangID uint, aiModel string) (*model.ProductDescription, error) {
|
||||||
|
|
||||||
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productShopID, productFromLangID)
|
productDescription, err := s.productDescriptionRepo.GetProductDescription(productID, productFromLangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ package constdata
|
|||||||
|
|
||||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||||
|
const SHOP_ID = 1
|
||||||
|
const MAX_AMOUNT_OF_CARTS_PER_USER = 10
|
||||||
|
const DEFAULT_NEW_CART_NAME = "new cart"
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ func WhereFromStrings(column, conditionOperator, value string) Filter {
|
|||||||
value = strings.ReplaceAll(value, "~", "")
|
value = strings.ReplaceAll(value, "~", "")
|
||||||
|
|
||||||
filt = func(d *gorm.DB) *gorm.DB {
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
// return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
||||||
|
// (jeśli masz collation case-insensitive, np. utf8mb4_general_ci)
|
||||||
|
return d.Where(column+` LIKE ?`, "%"+value+"%")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,33 @@ func WhereFromStrings(column, conditionOperator, value string) Filter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle IN operator for comma-separated values (e.g., product_id_in=1,2,3,4)
|
||||||
|
if conditionOperator == "IN" {
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
var values []interface{}
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Try to parse as int first
|
||||||
|
if i, err := strconv.ParseInt(p, 10, 64); err == nil {
|
||||||
|
values = append(values, i)
|
||||||
|
} else if f, err := strconv.ParseFloat(p, 64); err == nil {
|
||||||
|
values = append(values, f)
|
||||||
|
} else {
|
||||||
|
values = append(values, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+" IN ?", values)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(value, "]") && strings.Contains(value, "[") {
|
if strings.Contains(value, "]") && strings.Contains(value, "[") {
|
||||||
period := strings.ReplaceAll(value, "[", "")
|
period := strings.ReplaceAll(value, "[", "")
|
||||||
period = strings.ReplaceAll(period, "]", "")
|
period = strings.ReplaceAll(period, "]", "")
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Paging struct {
|
type Paging struct {
|
||||||
Page uint `json:"page_number" example:"5"`
|
Page uint `json:"p" example:"5"`
|
||||||
Elements uint `json:"elements_per_page" example:"30"`
|
Elements uint `json:"elems" example:"30"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Paging) Offset() int {
|
func (p Paging) Offset() int {
|
||||||
|
|||||||
@@ -59,5 +59,8 @@ func ParseFieldFilters[T any](c fiber.Ctx, formColumnMapping ...map[string]strin
|
|||||||
func ParsePagination(c fiber.Ctx) (uint, uint) {
|
func ParsePagination(c fiber.Ctx) (uint, uint) {
|
||||||
pageNum, _ := strconv.ParseInt(c.Query("p", "1"), 10, 64)
|
pageNum, _ := strconv.ParseInt(c.Query("p", "1"), 10, 64)
|
||||||
pageSize, _ := strconv.ParseInt(c.Query("elems", "30"), 10, 64)
|
pageSize, _ := strconv.ParseInt(c.Query("elems", "30"), 10, 64)
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
return uint(pageNum), uint(pageSize)
|
return uint(pageNum), uint(pageSize)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func ParseWhereScopes[T any](c fiber.Ctx, ignoredKeys []string, formColumnMappin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractOperator(key string) (base string, operatorSuffix string) {
|
func extractOperator(key string) (base string, operatorSuffix string) {
|
||||||
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq"}
|
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq", "_in"}
|
||||||
for _, suf := range suffixes {
|
for _, suf := range suffixes {
|
||||||
if strings.HasSuffix(key, suf) {
|
if strings.HasSuffix(key, suf) {
|
||||||
return strings.TrimSuffix(key, suf), suf[1:]
|
return strings.TrimSuffix(key, suf), suf[1:]
|
||||||
@@ -69,6 +69,8 @@ func resolveOperator(suffix string) string {
|
|||||||
return "!="
|
return "!="
|
||||||
case "eq":
|
case "eq":
|
||||||
return "="
|
return "="
|
||||||
|
case "in":
|
||||||
|
return "IN"
|
||||||
default:
|
default:
|
||||||
return "LIKE"
|
return "LIKE"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ var (
|
|||||||
|
|
||||||
// Typed errors for menu handler
|
// Typed errors for menu handler
|
||||||
ErrNoRootFound = errors.New("no root found in categories table")
|
ErrNoRootFound = errors.New("no root found in categories table")
|
||||||
|
|
||||||
|
// Typed errors for carts handler
|
||||||
|
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
|
||||||
|
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
|
||||||
|
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents an error with HTTP status code
|
// Error represents an error with HTTP status code
|
||||||
@@ -141,6 +146,13 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrNoRootFound):
|
case errors.Is(err, ErrNoRootFound):
|
||||||
return i18n.T_(c, "error.no_root_found")
|
return i18n.T_(c, "error.no_root_found")
|
||||||
|
|
||||||
|
case errors.Is(err, ErrMaxAmtOfCartsReached):
|
||||||
|
return i18n.T_(c, "error.max_amt_of_carts_reached")
|
||||||
|
case errors.Is(err, ErrUserHasNoSuchCart):
|
||||||
|
return i18n.T_(c, "error.user_has_no_such_cart")
|
||||||
|
case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||||
|
return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return i18n.T_(c, "error.err_internal_server_error")
|
return i18n.T_(c, "error.err_internal_server_error")
|
||||||
}
|
}
|
||||||
@@ -176,7 +188,10 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrBadField),
|
errors.Is(err, ErrBadField),
|
||||||
errors.Is(err, ErrInvalidXHTML),
|
errors.Is(err, ErrInvalidXHTML),
|
||||||
errors.Is(err, ErrBadPaging),
|
errors.Is(err, ErrBadPaging),
|
||||||
errors.Is(err, ErrNoRootFound):
|
errors.Is(err, ErrNoRootFound),
|
||||||
|
errors.Is(err, ErrMaxAmtOfCartsReached),
|
||||||
|
errors.Is(err, ErrUserHasNoSuchCart),
|
||||||
|
errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
|
|||||||
8
bo/components.d.ts
vendored
8
bo/components.d.ts
vendored
@@ -11,14 +11,21 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
Cart1: typeof import('./src/components/customer/Cart1.vue')['default']
|
||||||
|
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
|
||||||
|
CompanyAccountView: typeof import('./src/components/customer/CompanyAccountView.vue')['default']
|
||||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
|
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
|
||||||
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
|
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
|
||||||
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
||||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||||
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
|
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
|
||||||
|
PageAccount: typeof import('./src/components/customer/PageAccount.vue')['default']
|
||||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||||
|
PageCreateAccount: typeof import('./src/components/customer/PageCreateAccount.vue')['default']
|
||||||
|
PageCustomerData: typeof import('./src/components/customer/PageCustomerData.vue')['default']
|
||||||
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||||
|
PageProductsList: typeof import('./src/components/customer/PageProductsList.vue')['default']
|
||||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||||
@@ -41,6 +48,7 @@ declare module 'vue' {
|
|||||||
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||||
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
||||||
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||||
|
UNavigationMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
|
||||||
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
||||||
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||||
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||||
|
|||||||
@@ -28,9 +28,18 @@ const authStore = useAuthStore()
|
|||||||
<RouterLink :to="{ name: 'addresses' }">
|
<RouterLink :to="{ name: 'addresses' }">
|
||||||
Addresses
|
Addresses
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'customer-data' }">
|
||||||
|
Customer Data
|
||||||
|
</RouterLink>
|
||||||
<RouterLink :to="{ name: 'cart' }">
|
<RouterLink :to="{ name: 'cart' }">
|
||||||
Cart
|
Cart
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'cart1' }">
|
||||||
|
Cart1
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'products-list' }">
|
||||||
|
Products List
|
||||||
|
</RouterLink>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const authStore = useAuthStore()
|
|||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
|
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
|
||||||
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
|
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
|
||||||
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
<span class="font-semibold text-gray-900 dark:text-white">B2B</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<!-- Right Side Actions -->
|
<!-- Right Side Actions -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
<component :is="Default || 'div'">
|
||||||
<div class="container my-10 mx-auto ">
|
<div class="container my-10 mx-auto ">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -26,7 +28,6 @@
|
|||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
|
||||||
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
|
<div 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" />
|
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -121,6 +123,7 @@ import { useProductStore } from '@/stores/product'
|
|||||||
import { useEditable } from '@/composable/useConteditable'
|
import { useEditable } from '@/composable/useConteditable'
|
||||||
import { langs } from '@/router/langs'
|
import { langs } from '@/router/langs'
|
||||||
import type { Language } from '@/types'
|
import type { Language } from '@/types'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
const activeTab = ref('description')
|
const activeTab = ref('description')
|
||||||
const productStore = useProductStore()
|
const productStore = useProductStore()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useProductStore, type Product } from '@/stores/product'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -153,9 +154,10 @@ function clearFilters() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
||||||
<div >
|
<div>
|
||||||
<!-- v-html="productStore.products" -->
|
<!-- v-html="productStore.products" -->
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
|
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
|
||||||
@@ -176,50 +178,33 @@ function clearFilters() {
|
|||||||
|
|
||||||
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
|
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
|
||||||
<!-- Filter Block -->
|
<!-- Filter Block -->
|
||||||
<div class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
|
<div
|
||||||
|
class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
|
||||||
<div class="flex flex-col min-w-[180px]">
|
<div class="flex flex-col min-w-[180px]">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name') }}</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name')
|
||||||
<UInput
|
}}</label>
|
||||||
v-model="searchName"
|
<UInput v-model="searchName" :placeholder="t('products.search_name_placeholder')"
|
||||||
:placeholder="t('products.search_name_placeholder')"
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
@update:model-value="resetPage"
|
|
||||||
class="dark:text-white text-black"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[180px]">
|
<div class="flex flex-col min-w-[180px]">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code') }}</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code')
|
||||||
<UInput
|
}}</label>
|
||||||
v-model="searchCode"
|
<UInput v-model="searchCode" :placeholder="t('products.search_code_placeholder')"
|
||||||
:placeholder="t('products.search_code_placeholder')"
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
@update:model-value="resetPage"
|
|
||||||
class="dark:text-white text-black"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[120px]">
|
<div class="flex flex-col min-w-[120px]">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
|
||||||
<UInput
|
<UInput v-model="priceFromFilter" type="number" :placeholder="t('products.price_from')"
|
||||||
v-model="priceFromFilter"
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
type="number"
|
|
||||||
:placeholder="t('products.price_from')"
|
|
||||||
@update:model-value="resetPage"
|
|
||||||
class="dark:text-white text-black"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[120px]">
|
<div class="flex flex-col min-w-[120px]">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
|
||||||
<UInput
|
<UInput v-model="priceToFilter" type="number" :placeholder="t('products.price_to')"
|
||||||
v-model="priceToFilter"
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
type="number"
|
|
||||||
:placeholder="t('products.price_to')"
|
|
||||||
@update:model-value="resetPage"
|
|
||||||
class="dark:text-white text-black"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<button
|
<button @click="clearFilters"
|
||||||
@click="clearFilters"
|
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||||
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
{{ t('products.clear_filters') }}
|
{{ t('products.clear_filters') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,4 +239,5 @@ function clearFilters() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
74
bo/src/components/customer/Cart1.vue
Normal file
74
bo/src/components/customer/Cart1.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<h2
|
||||||
|
class="font-semibold text-black dark:text-white pb-6 text-2xl">
|
||||||
|
{{ t('Cart Items') }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||||
|
<div v-if="cartStore.items.length > 0" class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
|
||||||
|
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center justify-between p-4 gap-4">
|
||||||
|
<div class="grid grid-cols-5 w-[100%]">
|
||||||
|
<div
|
||||||
|
class="w-20 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||||
|
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||||
|
<UIcon v-else name="mdi:package-variant" class="text-xl text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.name }}</p>
|
||||||
|
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.product_number }}</p>
|
||||||
|
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</p>
|
||||||
|
<div class="flex items-center justify-end gap-10">
|
||||||
|
<UInputNumber v-model="item.quantity" class="text-gray-500 dark:text-gray-400 text-sm" />
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button @click="removeItem(item.id)"
|
||||||
|
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
|
||||||
|
:title="t('Remove')">
|
||||||
|
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="p-8 text-center">
|
||||||
|
<UIcon name="mdi:cart-outline" class="text-5xl text-gray-300 dark:text-gray-600 mb-4 mx-auto" />
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="cartStore.items.length > 0" class="flex gap-4 justify-end items-center pt-6">
|
||||||
|
<UButton color="primary" @click="handleContinueToCheckout"
|
||||||
|
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||||
|
{{ t('Continue to Checkout') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" color="neutral" @click="handleCancel"
|
||||||
|
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCartStore } from '@/stores/cart'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
const cartStore = useCartStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
function handleCancel() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContinueToCheckout() {
|
||||||
|
router.push({ name: 'cart' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(itemId: number) {
|
||||||
|
cartStore.removeItem(itemId)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto mt-10">
|
<component :is="Default || 'div'">
|
||||||
<div class="flex flex-col mb-6">
|
<div class="container mx-auto mt-20">
|
||||||
|
<div class="flex flex-col gap-5 mb-6">
|
||||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
|
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
|
||||||
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
|
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
|
||||||
@@ -16,30 +17,32 @@
|
|||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div v-for="address in paginatedAddresses" :key="address.id"
|
<div v-for="address in paginatedAddresses" :key="address.id"
|
||||||
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow">
|
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow flex justify-between">
|
||||||
<p class="text-black dark:text-white">{{ address.street }}</p>
|
<div class="flex flex-col gap-2 items-strat justify-end">
|
||||||
<p class="text-black dark:text-white">{{ address.zipCode }}, {{ address.city }}</p>
|
<p class="text-black dark:text-white">{{ address.street }}</p>
|
||||||
<p class="text-black dark:text-white">{{ address.country }}</p>
|
<p class="text-black dark:text-white">{{ address.zipCode }}, {{ address.city }}</p>
|
||||||
<div class="flex gap-2 mt-2">
|
<p class="text-black dark:text-white">{{ address.country }}</p>
|
||||||
<UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)"
|
</div>
|
||||||
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark)">{{ t('edit') }}
|
<div class="flex flex-col items-end justify-between gap-2">
|
||||||
</UButton>
|
<button @click="confirmDelete(address.id)"
|
||||||
<button size="sm" color="destructive" variant="outline" @click="confirmDelete(address.id)"
|
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
|
||||||
class="text-red-500 hover:bg-red-100 dark:hover:bg-red-900 dark:hover:text-red-100 rounded transition-colors p-2">
|
:title="t('Remove')">
|
||||||
<UIcon name="material-symbols:delete" class="text-[16px]" />
|
<UIcon name="material-symbols:delete" class="text-[18px]" />
|
||||||
</button>
|
</button>
|
||||||
|
<UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)"
|
||||||
|
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[13px]">
|
||||||
|
{{ t('edit') }}
|
||||||
|
<UIcon name="ic:sharp-edit" class="text-[15px]" />
|
||||||
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
|
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-center">
|
<div class="mt-6 flex justify-center">
|
||||||
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
|
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
|
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-6 flex flex-col gap-6">
|
<div class="p-6 flex flex-col gap-6">
|
||||||
@@ -74,7 +77,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|
||||||
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
|
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-6 flex flex-col gap-3">
|
<div class="p-6 flex flex-col gap-3">
|
||||||
@@ -97,16 +99,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useAddressStore } from '@/stores/address'
|
import { useAddressStore } from '@/stores/address'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
const addressStore = useAddressStore()
|
const addressStore = useAddressStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
@@ -122,17 +124,14 @@ const totalItems = computed(() => addressStore.totalItems)
|
|||||||
const pageSize = addressStore.pageSize
|
const pageSize = addressStore.pageSize
|
||||||
|
|
||||||
watch(page, (newPage) => addressStore.setPage(newPage))
|
watch(page, (newPage) => addressStore.setPage(newPage))
|
||||||
|
|
||||||
watch(searchQuery, (val) => {
|
watch(searchQuery, (val) => {
|
||||||
addressStore.setSearchQuery(val)
|
addressStore.setSearchQuery(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
resetForm()
|
resetForm()
|
||||||
isEditing.value = false
|
isEditing.value = false
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(address: any) {
|
function openEditModal(address: any) {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
street: address.street,
|
street: address.street,
|
||||||
@@ -144,17 +143,14 @@ function openEditModal(address: any) {
|
|||||||
editingAddressId.value = address.id
|
editingAddressId.value = address.id
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
formData.value = { street: '', zipCode: '', city: '', country: '' }
|
formData.value = { street: '', zipCode: '', city: '', country: '' }
|
||||||
editingAddressId.value = null
|
editingAddressId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
const errors = []
|
const errors = []
|
||||||
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
|
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
|
||||||
@@ -163,7 +159,6 @@ function validate() {
|
|||||||
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
|
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
|
||||||
return errors.length ? errors : null
|
return errors.length ? errors : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAddress() {
|
function saveAddress() {
|
||||||
if (validate()) return
|
if (validate()) return
|
||||||
if (isEditing.value && editingAddressId.value) {
|
if (isEditing.value && editingAddressId.value) {
|
||||||
@@ -173,12 +168,10 @@ function saveAddress() {
|
|||||||
}
|
}
|
||||||
closeModal()
|
closeModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(id: number) {
|
function confirmDelete(id: number) {
|
||||||
addressToDelete.value = id
|
addressToDelete.value = id
|
||||||
showDeleteConfirm.value = true
|
showDeleteConfirm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAddress() {
|
function deleteAddress() {
|
||||||
if (addressToDelete.value) {
|
if (addressToDelete.value) {
|
||||||
addressStore.deleteAddress(addressToDelete.value)
|
addressStore.deleteAddress(addressToDelete.value)
|
||||||
|
|||||||
@@ -1,66 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto mt-20 px-4 py-8">
|
<component :is="Default || 'div'">
|
||||||
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
|
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
|
||||||
<div class="flex flex-col lg:flex-row gap-8 mb-8">
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
|
||||||
|
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
<div
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||||
|
<h2
|
||||||
|
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
{{ t('Selected Products') }}
|
{{ t('Selected Products') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
|
||||||
<div class="col-span-4">{{ t('Product') }}</div>
|
|
||||||
<div class="col-span-2 text-right">{{ t('Price') }}</div>
|
|
||||||
<div class="col-span-3 text-center">{{ t('Quantity') }}</div>
|
|
||||||
<div class="col-span-2 text-right">{{ t('Total') }}</div>
|
|
||||||
<div class="col-span-1 text-center">{{ t('Actions') }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="cartStore.items.length > 0">
|
<div v-if="cartStore.items.length > 0">
|
||||||
<div v-for="item in cartStore.items" :key="item.id"
|
<div v-for="item in cartStore.items" :key="item.id"
|
||||||
class="grid grid-cols-1 md:grid-cols-12 gap-4 p-4 border-b border-(--border-light) dark:border-(--border-dark) items-center">
|
class="grid grid-cols-5 items-center p-4 border-b border-(--border-light) dark:border-(--border-dark) w-[100%]">
|
||||||
<div class="col-span-4 flex items-center gap-4">
|
<div
|
||||||
<div class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||||
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||||
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
||||||
</div>
|
|
||||||
<span class="text-black dark:text-white text-sm font-medium">{{ item.name }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 text-right">
|
<p class="text-black dark:text-white text-sm font-medium">{{ item.name }}</p>
|
||||||
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Price') }}: </span>
|
<p class="text-black dark:text-white">${{ item.price.toFixed(2) }}</p>
|
||||||
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
|
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
|
||||||
</div>
|
}}</p>
|
||||||
<div class="col-span-3 flex items-center justify-center">
|
|
||||||
<div class="flex items-center border border-(--border-light) dark:border-(--border-dark) rounded">
|
<div class="flex items-center justify-end gap-10">
|
||||||
<button @click="decreaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
<UInputNumber v-model="item.quantity" :min="1"
|
||||||
<UIcon name="mdi:minus" />
|
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
|
||||||
</button>
|
<div class="flex justify-center">
|
||||||
<span class="px-3 py-1 text-black dark:text-white min-w-[40px] text-center">{{ item.quantity }}</span>
|
<button @click="removeItem(item.id)"
|
||||||
<button @click="increaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
|
||||||
<UIcon name="mdi:plus" />
|
:title="t('Remove')">
|
||||||
|
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 text-right">
|
|
||||||
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Total') }}: </span>
|
|
||||||
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-1 flex justify-center">
|
|
||||||
<button @click="removeItem(item.id)" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" :title="t('Remove')">
|
|
||||||
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="p-8 text-center">
|
<div v-else class="p-8 text-center">
|
||||||
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
||||||
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||||
<RouterLink :to="{ name: 'product-card-full' }" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
<RouterLink :to="{ name: 'product-card-full' }"
|
||||||
|
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
{{ t('Continue Shopping') }}
|
{{ t('Continue Shopping') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-80">
|
<div class="lg:w-80">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
||||||
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
@@ -74,13 +62,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) }}%)</span>
|
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
|
||||||
|
}}%)</span>
|
||||||
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mb-6">
|
<div class="flex justify-between mb-6">
|
||||||
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
|
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
|
||||||
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{ cartStore.orderTotal.toFixed(2) }}</span>
|
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
|
||||||
|
cartStore.orderTotal.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
||||||
@@ -95,9 +85,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col lg:flex-row gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-10">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||||
@@ -105,8 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
|
||||||
:class="cartStore.selectedAddressId === address.id
|
|
||||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
<input type="radio" :value="address.id" v-model="selectedAddress"
|
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||||
@@ -121,19 +111,20 @@
|
|||||||
<div v-else class="text-center py-6">
|
<div v-else class="text-center py-6">
|
||||||
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||||
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||||
<RouterLink :to="{ name: 'addresses' }" class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
<RouterLink :to="{ name: 'addresses' }"
|
||||||
|
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
{{ t('Add Address') }}
|
{{ t('Add Address') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
||||||
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
|
||||||
:class="cartStore.selectedDeliveryMethodId === method.id
|
|
||||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
|
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
|
||||||
@@ -153,15 +144,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useCartStore, type CartItem } from '@/stores/cart'
|
import { useCartStore } from '@/stores/cart'
|
||||||
import { useAddressStore } from '@/stores/address'
|
import { useAddressStore } from '@/stores/address'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
const cartStore = useCartStore()
|
const cartStore = useCartStore()
|
||||||
const addressStore = useAddressStore()
|
const addressStore = useAddressStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -187,18 +179,9 @@ watch(selectedDeliveryMethod, (newValue) => {
|
|||||||
|
|
||||||
const canPlaceOrder = computed(() => {
|
const canPlaceOrder = computed(() => {
|
||||||
return cartStore.items.length > 0 &&
|
return cartStore.items.length > 0 &&
|
||||||
cartStore.selectedAddressId !== null &&
|
cartStore.selectedAddressId !== null &&
|
||||||
cartStore.selectedDeliveryMethodId !== null
|
cartStore.selectedDeliveryMethodId !== null
|
||||||
})
|
})
|
||||||
|
|
||||||
function increaseQuantity(item: CartItem) {
|
|
||||||
cartStore.updateQuantity(item.id, item.quantity + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function decreaseQuantity(item: CartItem) {
|
|
||||||
cartStore.updateQuantity(item.id, item.quantity - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(itemId: number) {
|
function removeItem(itemId: number) {
|
||||||
cartStore.removeItem(itemId)
|
cartStore.removeItem(itemId)
|
||||||
}
|
}
|
||||||
|
|||||||
180
bo/src/components/customer/PageCreateAccount.vue
Normal file
180
bo/src/components/customer/PageCreateAccount.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<div class="flex flex-col gap-5 mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Create Account') }}</h1>
|
||||||
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
|
||||||
|
<UForm @submit.prevent="saveAccount" :validate="validate" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<UIcon name="mdi:domain"
|
||||||
|
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
{{ t('Company Information') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||||
|
t('Company Name') }} *</label>
|
||||||
|
<UInput v-model="formData.companyName" :placeholder="t('Enter company name')"
|
||||||
|
name="companyName" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||||
|
t('Company Email') }} *</label>
|
||||||
|
<UInput v-model="formData.companyEmail" type="email"
|
||||||
|
:placeholder="t('Enter company email')" name="companyEmail" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{
|
||||||
|
t('REGON') }}</label>
|
||||||
|
<UInput v-model="formData.regon" :placeholder="t('Enter REGON')" name="regon"
|
||||||
|
class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('NIP')
|
||||||
|
}}</label>
|
||||||
|
<UInput v-model="formData.nip" :placeholder="t('Enter NIP')" name="nip"
|
||||||
|
class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">{{ t('VAT')
|
||||||
|
}}</label>
|
||||||
|
<UInput v-model="formData.vat" :placeholder="t('Enter VAT')" name="vat"
|
||||||
|
class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<UIcon name="mdi:map-marker"
|
||||||
|
class="text-[20px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
{{ t('Select Addresses') }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark)">
|
||||||
|
<div class="mb-4">
|
||||||
|
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||||
|
class="w-full bg-white dark:bg-(--black) text-black dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||||
|
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||||
|
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
||||||
|
:class="cartStore.selectedAddressId === address.id
|
||||||
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
|
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||||
|
class="mt-1 w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-black dark:text-white font-medium">{{ address.street }}</p>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.zipCode }},
|
||||||
|
{{ address.city }}</p>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 text-sm">{{ address.country }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-6">
|
||||||
|
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||||
|
<RouterLink :to="{ name: 'addresses' }"
|
||||||
|
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
|
{{ t('Add Address') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
|
<UButton variant="outline" color="neutral" @click="goBack"
|
||||||
|
class="text-black dark:text-white">
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton type="submit" color="primary"
|
||||||
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||||
|
<UIcon name="mdi:content-save" />
|
||||||
|
{{ t('Save') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useCustomerStore } from '@/stores/customer'
|
||||||
|
import { useAddressStore } from '@/stores/address'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useCartStore } from '@/stores/cart'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
const router = useRouter()
|
||||||
|
const customerStore = useCustomerStore()
|
||||||
|
const addressStore = useAddressStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const cartStore = useCartStore()
|
||||||
|
const formData = ref({
|
||||||
|
companyName: customerStore.customer?.companyName || '',
|
||||||
|
companyEmail: customerStore.customer?.companyEmail || '',
|
||||||
|
regon: customerStore.customer?.regon || '',
|
||||||
|
nip: customerStore.customer?.nip || '',
|
||||||
|
vat: customerStore.customer?.vat || '',
|
||||||
|
companyAddressId: customerStore.customer?.companyAddressId || null,
|
||||||
|
billingAddressId: customerStore.customer?.billingAddressId || null
|
||||||
|
})
|
||||||
|
|
||||||
|
const addressSearchQuery = ref('')
|
||||||
|
|
||||||
|
watch(addressSearchQuery, (val) => {
|
||||||
|
addressStore.setSearchQuery(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedAddress = ref<number | null>(formData.value.companyAddressId)
|
||||||
|
|
||||||
|
watch(selectedAddress, (newValue) => {
|
||||||
|
formData.value.companyAddressId = newValue
|
||||||
|
})
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const errors: { name: string; message: string }[] = []
|
||||||
|
|
||||||
|
if (!formData.value.companyName?.trim()) {
|
||||||
|
errors.push({ name: 'companyName', message: t('Company name is required') })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.value.companyEmail?.trim()) {
|
||||||
|
errors.push({ name: 'companyEmail', message: t('Company email is required') })
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAccount() {
|
||||||
|
const errors = validate()
|
||||||
|
if (errors.length) return
|
||||||
|
|
||||||
|
const selectedAddr = addressStore.addresses.find(
|
||||||
|
addr => addr.id === formData.value.companyAddressId
|
||||||
|
)
|
||||||
|
|
||||||
|
customerStore.setCustomer({
|
||||||
|
companyName: formData.value.companyName,
|
||||||
|
companyEmail: formData.value.companyEmail,
|
||||||
|
regon: formData.value.regon,
|
||||||
|
nip: formData.value.nip,
|
||||||
|
vat: formData.value.vat,
|
||||||
|
companyAddressId: formData.value.companyAddressId,
|
||||||
|
billingAddressId: formData.value.billingAddressId,
|
||||||
|
companyAddress: selectedAddr || null
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push({ name: 'customer-data' })
|
||||||
|
}
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'customer-data' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
123
bo/src/components/customer/PageCustomerData.vue
Normal file
123
bo/src/components/customer/PageCustomerData.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<div class="flex flex-col gap-5 mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Customer Data') }}</h1>
|
||||||
|
|
||||||
|
<div v-if="!customerStore.hasAccount" class="flex flex-col items-center justify-center py-12">
|
||||||
|
<div class="text-center flex flex-col items-center justify-center mb-6">
|
||||||
|
<UIcon name="mdi:domain" class="text-[60px] text-gray-400 dark:text-gray-500" />
|
||||||
|
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">{{ t('No customer account found') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-500">{{ t('Create an account to manage your company data') }}</p>
|
||||||
|
</div>
|
||||||
|
<UButton color="primary" @click="goToCreateAccount"
|
||||||
|
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
|
||||||
|
<UIcon name="mdi:add-bold" />
|
||||||
|
{{ t('Create Account') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col gap-3">
|
||||||
|
<div class="grid grid-cols-2 gap-5">
|
||||||
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-4">
|
||||||
|
<h2 class="text-xl font-semibold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<UIcon name="mdi:domain"
|
||||||
|
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
{{ t('Company Information') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-10">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{ t('Company Name') }}</label>
|
||||||
|
<p class="text-black dark:text-white">{{ customerStore.customer?.companyName || '-'}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{
|
||||||
|
t('Company Email') }}</label>
|
||||||
|
<p class="text-black dark:text-white">{{ customerStore.customer?.companyEmail || '-'}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{ t('REGON')}}</label>
|
||||||
|
<p class="text-black dark:text-white">{{ customerStore.customer?.regon || '-' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('NIP')}}</label>
|
||||||
|
<p class="text-black dark:text-white">{{ customerStore.customer?.nip || '-' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">{{t('VAT')}}</label>
|
||||||
|
<p class="text-black dark:text-white">{{ customerStore.customer?.vat || '-' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="text-xl font-semibold text-black dark:text-white mb-2 flex items-center gap-2">
|
||||||
|
<UIcon name="mdi:map-marker"
|
||||||
|
class="text-[24px] text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
{{ t('Addresses') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">{{ t('Company Address') }}</label>
|
||||||
|
<div v-if="companyAddress"
|
||||||
|
class="p-4 bg-white dark:bg-(--black) rounded-md border border-(--border-light) dark:border-(--border-dark)">
|
||||||
|
<p class="text-black dark:text-white">{{ companyAddress.street }}</p>
|
||||||
|
<p class="text-black dark:text-white">{{ companyAddress.zipCode }},
|
||||||
|
{{ companyAddress.city }}</p>
|
||||||
|
<p class="text-black dark:text-white">{{ companyAddress.country }}</p>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-400 dark:text-gray-500">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<UButton color="primary" variant="outline"
|
||||||
|
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) border-(--accent-blue-light) dark:border-(--accent-blue-dark)"
|
||||||
|
@click="goToCreateAccount">
|
||||||
|
<UIcon name="ic:sharp-edit" />
|
||||||
|
{{ t('Edit Account') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useCustomerStore } from '@/stores/customer'
|
||||||
|
import { useAddressStore } from '@/stores/address'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
const router = useRouter()
|
||||||
|
const customerStore = useCustomerStore()
|
||||||
|
const addressStore = useAddressStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const companyAddress = computed(() => {
|
||||||
|
const id = customerStore.customer?.companyAddressId
|
||||||
|
return id ? addressStore.getAddressById(id) : null
|
||||||
|
})
|
||||||
|
|
||||||
|
function goToCreateAccount() {
|
||||||
|
router.push({ name: 'create-account' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mt-14 mx-auto">
|
<component :is="Default || 'div'">
|
||||||
<div class="flex justify-between gap-8 mb-6">
|
<div class="container mt-20 mx-auto">
|
||||||
|
<div class="flex md:flex-row flex-col justify-between gap-8 my-6">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||||
class="max-w-full h-auto object-contain" />
|
class="max-w-full h-auto object-contain" />
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-end mb-8">
|
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -49,10 +50,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<ProductCustomization />
|
<ProductCustomization />
|
||||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
<div class="mb-6 w-[55%]">
|
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||||
<div class="flex justify-between items-center gap-10 mb-8">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||||
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||||
'px-15 py-2 cursor-pointer',
|
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!',
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
@@ -69,13 +70,14 @@
|
|||||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
<ProductVariants />
|
<ProductVariants />
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import ProductCustomization from './components/ProductCustomization.vue'
|
import ProductCustomization from './components/ProductCustomization.vue'
|
||||||
import ProductVariants from './components/ProductVariants.vue'
|
import ProductVariants from './components/ProductVariants.vue'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
interface Color {
|
interface Color {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
112
bo/src/components/customer/PageProductsList.vue
Normal file
112
bo/src/components/customer/PageProductsList.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<suspense>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<!-- <div class="w-64 h-128">
|
||||||
|
<CategoryMenu />
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
|
||||||
|
<div v-if="loading" class="text-center py-8">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Image</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Product ID</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Name</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<tr v-for="product in productsList" :key="product.product_id"
|
||||||
|
class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<img :src="product.ImageID" alt="product image"
|
||||||
|
class="w-16 h-16 object-cover rounded" />
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||||
|
product.product_id }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||||
|
product.name }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600 dark:text-blue-400">
|
||||||
|
{{ product.LinkRewrite }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="flex justify-center items-center py-8">
|
||||||
|
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||||
|
</div>
|
||||||
|
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
No products found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</suspense>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
interface Product {
|
||||||
|
product_id: number
|
||||||
|
name: string
|
||||||
|
ImageID: string
|
||||||
|
LinkRewrite: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const perPage = ref(15)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
message: string
|
||||||
|
items: Product[]
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const productsList = ref<Product[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function fetchProductList() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useFetchJson(
|
||||||
|
`api/v1/restricted/list-products/get-listing?p=${page.value}&elems=${perPage.value}`
|
||||||
|
) as ApiResponse
|
||||||
|
|
||||||
|
productsList.value = response.items || []
|
||||||
|
total.value = response.count || 0
|
||||||
|
} catch (e: unknown) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(page, () => {
|
||||||
|
fetchProductList()
|
||||||
|
})
|
||||||
|
onMounted(fetchProductList)
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="text-[24px] font-bold">Product customization</p>
|
<p class="text-[24px] font-bold">Product customization</p>
|
||||||
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-10">
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-5 md:gap-10 ">
|
||||||
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
||||||
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
||||||
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
||||||
|
|||||||
40
bo/src/components/inner/categoryMenu.vue
Normal file
40
bo/src/components/inner/categoryMenu.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getMenu } from '@/router/menu'
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
let menu = await getMenu() as NavigationMenuItem[]
|
||||||
|
|
||||||
|
const openAll = ref(false)
|
||||||
|
|
||||||
|
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||||
|
for (const item of menu) {
|
||||||
|
if(item.children && item.children.length > 0){
|
||||||
|
console.log(item);
|
||||||
|
adaptMenu(item.children);
|
||||||
|
item.open = openAll.value
|
||||||
|
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label ,to: { name: 'category', params: item.params }})
|
||||||
|
} else {
|
||||||
|
item.to = { name: 'category', params: item.params };
|
||||||
|
item.icon = 'i-lucide-file-text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = adaptMenu(menu)
|
||||||
|
|
||||||
|
const items = ref<NavigationMenuItem[][]>([
|
||||||
|
[
|
||||||
|
...menu as NavigationMenuItem[]
|
||||||
|
],
|
||||||
|
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4">
|
||||||
|
</UNavigationMenu>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TopBar from '@/components/TopBar.vue';
|
import TopBar from '@/components/TopBar.vue';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
<main class="p-10">
|
||||||
<main class="p-10">
|
<TopBar/>
|
||||||
<TopBar/>
|
<slot></slot>
|
||||||
<router-view />
|
</main>
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<main :key="i18n.locale.value">
|
<main :key="i18n.locale.value">
|
||||||
<TopBarLogin />
|
<TopBarLogin />
|
||||||
<router-view />
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Empty from '@/layouts/empty.vue'
|
|
||||||
import { currentLang, langs } from './langs'
|
import { currentLang, langs } from './langs'
|
||||||
import { getSettings } from './settings'
|
import { getSettings } from './settings'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import Default from '@/layouts/default.vue'
|
import Default from '@/layouts/default.vue'
|
||||||
|
import { getMenu } from './menu'
|
||||||
|
|
||||||
|
|
||||||
function isAuthenticated(): boolean {
|
function isAuthenticated(): boolean {
|
||||||
if (typeof document === 'undefined') return false
|
if (typeof document === 'undefined') return false
|
||||||
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await getSettings()
|
await getSettings()
|
||||||
|
// await getMenu()
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -24,61 +24,94 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/:locale',
|
path: '/:locale',
|
||||||
children: [
|
children: [
|
||||||
|
{ path: 'category/:category_id-:link_rewrite', component: () => import('@/views/CategoryView.vue'), name: 'category' },
|
||||||
|
{ path: 'products-datail', component: () => import('@/components/admin/ProductDetailView.vue'), name: 'product-detail' },
|
||||||
|
{ path: '', component: () => import('@/views/RepoChartView.vue'), name: 'home' },
|
||||||
|
{ path: 'products', component: () => import('@/components/admin/ProductsView.vue'), name: 'products' },
|
||||||
|
{ path: 'product-card-full', component: () => import('@/components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
||||||
|
{ path: 'addresses', component: () => import('@/components/customer/PageAddresses.vue'), name: 'addresses' },
|
||||||
|
{ path: 'customer-data', component: () => import('@/components/customer/PageCustomerData.vue'), name: 'customer-data' },
|
||||||
|
{ path: 'create-account', component: () => import('@/components/customer/PageCreateAccount.vue'), name: 'create-account' },
|
||||||
|
{ path: 'cart', component: () => import('@/components/customer/PageCart.vue'), name: 'cart' },
|
||||||
|
{ path: 'cart1', component: () => import('@/components/customer/Cart1.vue'), name: 'cart1' },
|
||||||
|
{ path: 'products-list', component: () => import('@/components/customer/PageProductsList.vue'), name: 'products-list' },
|
||||||
|
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true, } },
|
||||||
|
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
|
||||||
|
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
|
||||||
|
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
|
||||||
|
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/:pathMatch(.*)*',
|
||||||
component: Default,
|
component: () => import('@/views/NotFoundView.vue'),
|
||||||
children: [
|
name: 'not-found',
|
||||||
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
|
||||||
{ path: 'products', component: () => import('../components/admin/ProductsView.vue'), name: 'products' },
|
|
||||||
{ path: 'products-datail/', component: () => import('../components/admin/ProductDetailView.vue'), name: 'product-detail' },
|
|
||||||
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
|
||||||
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
|
||||||
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: Empty,
|
|
||||||
children: [
|
|
||||||
{ path: 'login', component: () => import('@/views/LoginView.vue'), name: 'login', meta: { guest: true } },
|
|
||||||
{ path: 'register', component: () => import('@/views/RegisterView.vue'), name: 'register', meta: { guest: true } },
|
|
||||||
{ path: 'password-recovery', component: () => import('@/views/PasswordRecoveryView.vue'), name: 'password-recovery', meta: { guest: true } },
|
|
||||||
{ path: 'reset-password', component: () => import('@/views/ResetPasswordForm.vue'), name: 'reset-password', meta: { guest: true } },
|
|
||||||
{ path: 'verify-email', component: () => import('@/views/VerifyEmailView.vue'), name: 'verify-email', meta: { guest: true } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
component: () => import('@/views/NotFoundView.vue'),
|
||||||
|
name: 'not-found',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
// router.beforeEach((to, from, next) => {
|
||||||
|
// const locale = to.params.locale as string
|
||||||
|
// const localeLang = langs.find((x) => x.iso_code == locale)
|
||||||
|
|
||||||
|
// if (locale && langs.length > 0) {
|
||||||
|
// const authStore = useAuthStore()
|
||||||
|
// // console.log(authStore.isAuthenticated, to, from)
|
||||||
|
// // if()
|
||||||
|
// const validLocale = langs.find((l) => l.lang_code === locale)
|
||||||
|
|
||||||
|
// if (validLocale) {
|
||||||
|
// currentLang.value = localeLang
|
||||||
|
// if (!to.meta?.guest && !isAuthenticated()) {
|
||||||
|
// return next({ name: 'login', params: { locale } })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // return next()
|
||||||
|
// return next()
|
||||||
|
// } else if (locale) {
|
||||||
|
// return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!locale && to.path !== '/') {
|
||||||
|
// return next(`/${currentLang.value?.iso_code}${to.path}`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// next()
|
||||||
|
// })
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
const locale = to.params.locale as string
|
const locale = to.params.locale as string
|
||||||
const localeLang = langs.find((x) => x.iso_code == locale)
|
const localeLang = langs.find((x) => x.iso_code === locale)
|
||||||
|
|
||||||
if (locale && langs.length > 0) {
|
if (locale && langs.length > 0) {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
console.log(authStore.isAuthenticated, to, from)
|
|
||||||
// if()
|
|
||||||
const validLocale = langs.find((l) => l.lang_code === locale)
|
const validLocale = langs.find((l) => l.lang_code === locale)
|
||||||
|
|
||||||
if (validLocale) {
|
if (validLocale) {
|
||||||
currentLang.value = localeLang
|
currentLang.value = localeLang
|
||||||
if (!to.meta?.guest && !isAuthenticated()) {
|
|
||||||
return next({ name: 'login', params: { locale } })
|
if (!to.meta?.guest && !authStore.isAuthenticated) {
|
||||||
|
return { name: 'login', params: { locale } }
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return true
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
return `/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!locale && to.path !== '/') {
|
if (!locale && to.path !== '/') {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
return `/${currentLang.value?.iso_code}${to.path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
17
bo/src/router/menu.ts
Normal file
17
bo/src/router/menu.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useFetchJson } from "@/composable/useFetchJson";
|
||||||
|
import type { MenuItem, Route } from "@/types/menu";
|
||||||
|
|
||||||
|
export const getMenu = async () => {
|
||||||
|
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-menu');
|
||||||
|
|
||||||
|
return resp.items.children
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getRoutes = async () => {
|
||||||
|
const resp = await useFetchJson<Route[]>('/api/v1/restricted/menu/get-routes');
|
||||||
|
|
||||||
|
return resp.items
|
||||||
|
|
||||||
|
}
|
||||||
@@ -95,7 +95,6 @@ export const useAddressStore = defineStore('address', () => {
|
|||||||
id: existing.id,
|
id: existing.id,
|
||||||
...normalize(formData)
|
...normalize(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
function deleteAddress(id: number): boolean {
|
function deleteAddress(id: number): boolean {
|
||||||
|
|||||||
@@ -77,14 +77,42 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
password: string,
|
password: string,
|
||||||
confirm_password: string,
|
confirm_password: string,
|
||||||
lang?: string,
|
lang?: string,
|
||||||
|
company_name?: string,
|
||||||
|
company_email?: string,
|
||||||
|
company_address?: {
|
||||||
|
street: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
},
|
||||||
|
regon?: string,
|
||||||
|
nip?: string,
|
||||||
|
vat?: string,
|
||||||
|
billing_address?: {
|
||||||
|
street: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
|
const body: any = { first_name, last_name, email, password, confirm_password, lang: lang || 'en' }
|
||||||
|
|
||||||
|
// Add company information if provided
|
||||||
|
if (company_name) body.company_name = company_name
|
||||||
|
if (company_email) body.company_email = company_email
|
||||||
|
if (company_address) body.company_address = company_address
|
||||||
|
if (regon) body.regon = regon
|
||||||
|
if (nip) body.nip = nip
|
||||||
|
if (vat) body.vat = vat
|
||||||
|
if (billing_address) body.billing_address = billing_address
|
||||||
|
|
||||||
await useFetchJson('/api/v1/public/auth/register', {
|
await useFetchJson('/api/v1/public/auth/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ first_name, last_name, email, password, confirm_password, lang: lang || 'en' }),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
|
|
||||||
return { success: true, requiresVerification: true }
|
return { success: true, requiresVerification: true }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface CartItem {
|
|||||||
image: string
|
image: string
|
||||||
price: number
|
price: number
|
||||||
quantity: number
|
quantity: number
|
||||||
|
product_number: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeliveryMethod {
|
export interface DeliveryMethod {
|
||||||
@@ -23,7 +24,7 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
const selectedDeliveryMethodId = ref<number | null>(null)
|
const selectedDeliveryMethodId = ref<number | null>(null)
|
||||||
const shippingCost = ref(0)
|
const shippingCost = ref(0)
|
||||||
const vatRate = ref(0.23) // 23% VAT
|
const vatRate = ref(0.23) // 23% VAT
|
||||||
|
const currentPage = ref(1)
|
||||||
const deliveryMethods = ref<DeliveryMethod[]>([
|
const deliveryMethods = ref<DeliveryMethod[]>([
|
||||||
{ id: 1, name: 'Standard Delivery', price: 0, description: '5-7 business days' },
|
{ id: 1, name: 'Standard Delivery', price: 0, description: '5-7 business days' },
|
||||||
{ id: 2, name: 'Express Delivery', price: 15, description: '2-3 business days' },
|
{ id: 2, name: 'Express Delivery', price: 15, description: '2-3 business days' },
|
||||||
@@ -32,9 +33,9 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
|
|
||||||
function initMockData() {
|
function initMockData() {
|
||||||
items.value = [
|
items.value = [
|
||||||
{ id: 1, productId: 101, name: 'Premium Widget Pro', image: '/img/product-1.jpg', price: 129.99, quantity: 2 },
|
{ id: 1, productId: 101, name: 'Premium Widget Pro', product_number: 'NC209/7000', image: '/img/product-1.jpg', price: 129.99, quantity: 2 },
|
||||||
{ id: 2, productId: 102, name: 'Ultra Gadget X', image: '/img/product-2.jpg', price: 89.50, quantity: 1 },
|
{ id: 2, productId: 102, name: 'Ultra Gadget X', product_number: 'NC234/6453', image: '/img/product-2.jpg', price: 89.50, quantity: 1 },
|
||||||
{ id: 3, productId: 103, name: 'Mega Tool Set', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
|
{ id: 3, productId: 103, name: 'Mega Tool Set', product_number: 'NC324/9030', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +66,20 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteProduct(id: number): boolean {
|
||||||
|
const index = items.value.findIndex(a => a.id === id)
|
||||||
|
if (index === -1) return false
|
||||||
|
|
||||||
|
items.value.splice(index, 1)
|
||||||
|
resetProductPagination()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProductPagination() {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
function removeItem(itemId: number) {
|
function removeItem(itemId: number) {
|
||||||
const index = items.value.findIndex(i => i.id === itemId)
|
const index = items.value.findIndex(i => i.id === itemId)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -104,6 +119,7 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
vatAmount,
|
vatAmount,
|
||||||
orderTotal,
|
orderTotal,
|
||||||
itemCount,
|
itemCount,
|
||||||
|
deleteProduct,
|
||||||
updateQuantity,
|
updateQuantity,
|
||||||
removeItem,
|
removeItem,
|
||||||
clearCart,
|
clearCart,
|
||||||
|
|||||||
40
bo/src/stores/category.ts
Normal file
40
bo/src/stores/category.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{name: "product name", price: 342432},
|
||||||
|
{name: "product name", price: 342432},
|
||||||
|
{name: "product name", price: 342432},
|
||||||
|
{name: "product name", price: 342432},
|
||||||
|
{name: "product name", price: 342432},
|
||||||
|
]
|
||||||
|
|
||||||
|
// type CategoryProducts = {}
|
||||||
|
|
||||||
|
export const useCategoryStore = defineStore('category', () => {
|
||||||
|
const id_category = ref(0)
|
||||||
|
const categoryProducts = ref(products)
|
||||||
|
|
||||||
|
|
||||||
|
function setCategoryID(id: number) {
|
||||||
|
id_category.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCategoryProducts() {
|
||||||
|
return new Promise<typeof products>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Fetching products from category id: ', id_category.value);
|
||||||
|
resolve(categoryProducts.value)
|
||||||
|
}, 2000 * Math.random())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id_category,
|
||||||
|
getCategoryProducts,
|
||||||
|
setCategoryID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
46
bo/src/stores/customer.ts
Normal file
46
bo/src/stores/customer.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Address } from './address'
|
||||||
|
|
||||||
|
export interface CustomerData {
|
||||||
|
companyName: string
|
||||||
|
companyEmail: string
|
||||||
|
companyAddress: string
|
||||||
|
regon: string
|
||||||
|
nip: string
|
||||||
|
vat: string
|
||||||
|
billingAddressId: number | null
|
||||||
|
companyAddressId: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCustomerStore = defineStore('customer', () => {
|
||||||
|
const customer = ref<CustomerData | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const hasAccount = computed(() => customer.value !== null)
|
||||||
|
|
||||||
|
function setCustomer(data: CustomerData) {
|
||||||
|
customer.value = data
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCustomer() {
|
||||||
|
customer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCustomer(data: Partial<CustomerData>) {
|
||||||
|
if (customer.value) {
|
||||||
|
customer.value = { ...customer.value, ...data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customer,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
hasAccount,
|
||||||
|
setCustomer,
|
||||||
|
clearCustomer,
|
||||||
|
updateCustomer
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -42,7 +42,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProductDescription() {
|
async function saveProductDescription() {
|
||||||
try {
|
try {
|
||||||
const data = await useFetchJson(
|
const data = await useFetchJson(
|
||||||
`/api/v1/restricted/product-description/save-product-description?productID=1&productShopID=1&productLangID=1`,
|
`/api/v1/restricted/product-description/save-product-description?productID=1&productShopID=1&productLangID=1`,
|
||||||
@@ -92,6 +92,6 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
getProductDescription,
|
getProductDescription,
|
||||||
clearCurrentProduct,
|
clearCurrentProduct,
|
||||||
saveProductDescription,
|
saveProductDescription,
|
||||||
translateProductDescription
|
translateProductDescription,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
26
bo/src/types/menu.d.ts
vendored
Normal file
26
bo/src/types/menu.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export interface MenuItem {
|
||||||
|
category_id: number
|
||||||
|
label: string
|
||||||
|
params: Params
|
||||||
|
children: MenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Params {
|
||||||
|
category_id?: number
|
||||||
|
link_rewrite?: string
|
||||||
|
locale?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
ID: number
|
||||||
|
Name: string
|
||||||
|
Path: string
|
||||||
|
Component: string
|
||||||
|
Layout: string
|
||||||
|
Meta: string
|
||||||
|
IsActive: boolean
|
||||||
|
SortOrder: number
|
||||||
|
ParentID: any
|
||||||
|
Parent: any
|
||||||
|
Children: any
|
||||||
|
}
|
||||||
42
bo/src/views/CategoryView.vue
Normal file
42
bo/src/views/CategoryView.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mt-24">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="text-2xl">Category ID: {{ $route.params.category_id }}</h2>
|
||||||
|
<div v-for="(p, i) in products" :key="i">
|
||||||
|
<p>
|
||||||
|
<span class="border-b-1 bg-red-100 px-4">{{ p.name }}</span>
|
||||||
|
<span class="border-b-1 bg-red-100 px-4">{{ p.price }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// import { useRoute } from 'vue-router';
|
||||||
|
import Default from '@/layouts/default.vue';
|
||||||
|
import { useCategoryStore } from '@/stores/category';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
// const route = useRoute()
|
||||||
|
// console.log(route);
|
||||||
|
|
||||||
|
const categoryStore = useCategoryStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const products = ref([])
|
||||||
|
|
||||||
|
watch(() => route.params, async (n) => {
|
||||||
|
categoryStore.setCategoryID(parseInt(n.category_id as string))
|
||||||
|
const res = await categoryStore.getCategoryProducts()
|
||||||
|
products.value = res
|
||||||
|
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -4,8 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -52,9 +52,11 @@ const PrivacyComponent = computed(() =>
|
|||||||
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
|
import(`@/components/terms/${i18n.locale.value}_PrivacyPolicyView.vue`).catch(() => import('@/components/terms/en_PrivacyPolicyView.vue')),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<component :is="Empty || 'div'">
|
||||||
<UDrawer v-model:open="showTherms" :overlay="false">
|
<UDrawer v-model:open="showTherms" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="TermsComponent" />
|
<component :is="TermsComponent" />
|
||||||
@@ -63,7 +65,6 @@ const PrivacyComponent = computed(() =>
|
|||||||
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
|
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
</UDrawer>
|
||||||
<!-- PrivacyPolicyView -->
|
|
||||||
<UDrawer v-model:open="showPrivacy" :overlay="false">
|
<UDrawer v-model:open="showPrivacy" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="PrivacyComponent" />
|
<component :is="PrivacyComponent" />
|
||||||
@@ -76,9 +77,9 @@ const PrivacyComponent = computed(() =>
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
||||||
@@ -94,11 +95,12 @@ const PrivacyComponent = computed(() =>
|
|||||||
|
|
||||||
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
|
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
|
||||||
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
|
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
|
||||||
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
|
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)"
|
||||||
|
:ui="{ trailing: 'pe-1' }">
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||||
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
|
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
|
||||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
|
aria-controls="password" @click="showPassword = !showPassword" class="mr-2" />
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
@@ -114,15 +116,11 @@ const PrivacyComponent = computed(() =>
|
|||||||
{{ $t('general.sign_in') }}
|
{{ $t('general.sign_in') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="flex items-center gap-3 my-1">
|
<div class="flex items-center gap-3 my-1">
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Google Sign In -->
|
|
||||||
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
|
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
|
||||||
@click="authStore.loginWithGoogle()"
|
@click="authStore.loginWithGoogle()"
|
||||||
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
||||||
@@ -163,4 +161,5 @@ const PrivacyComponent = computed(() =>
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
7
bo/src/views/NotFoundView.vue
Normal file
7
bo/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center h-screen">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-800">404</h1>
|
||||||
|
<p class="mt-4 text-lg text-gray-600">Page not found</p>
|
||||||
|
<router-link to="/" class="mt-6 text-blue-500 hover:underline">Go back home</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -36,70 +37,74 @@ function validate(): FormError[] {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
<component :is="Empty || 'div'">
|
||||||
<div class="text-center mb-15">
|
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="text-center mb-15">
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
|
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
</div>
|
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
|
||||||
|
|
||||||
<template v-if="submitted">
|
<template v-if="submitted">
|
||||||
<div class="text-center flex flex-col gap-4">
|
<div class="text-center flex flex-col gap-4">
|
||||||
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
|
<UIcon name="i-heroicons-envelope" class="w-12 h-12 mx-auto text-primary-500" />
|
||||||
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
|
<h2 class="text-xl font-semibold dark:text-white text-black">{{ $t('general.check_your_email') }}</h2>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('general.password_reset_link_sent_notice') }}
|
{{ $t('general.password_reset_link_sent_notice') }}
|
||||||
</p>
|
</p>
|
||||||
<UButton color="neutral" variant="outline" block @click="goToLogin"
|
<UButton color="neutral" variant="outline" block @click="goToLogin"
|
||||||
class="dark:text-white text-black cursor-pointer">
|
class="dark:text-white text-black cursor-pointer">
|
||||||
{{ $t('general.back_to_sign_in') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('general.enter_email_for_password_reset') }}
|
{{ $t('general.enter_email_for_password_reset') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
|
<UForm :validate="validate" @submit="handleRecover" class="flex flex-col gap-3">
|
||||||
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
|
<UAlert v-if="authStore.error" color="error" variant="subtle" icon="i-heroicons-exclamation-triangle"
|
||||||
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
:title="authStore.error" :close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
||||||
@close="authStore.clearError" />
|
@close="authStore.clearError" />
|
||||||
|
|
||||||
<UFormField :label="$t('general.email_address')" name="email" required
|
<UFormField :label="$t('general.email_address')" name="email" required
|
||||||
class="w-full dark:text-white text-black">
|
class="w-full dark:text-white text-black">
|
||||||
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
|
<UInput v-model="email" :placeholder="$t('general.enter_your_email')" :disabled="authStore.loading"
|
||||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading"
|
<UButton type="submit" block :loading="authStore.loading"
|
||||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.send_password_reset_link') }}
|
{{ $t('general.send_password_reset_link') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
<div class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
<div
|
||||||
<button color="neutral" variant="outline" :loading="authStore.loading"
|
class="text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
||||||
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
<button color="neutral" variant="outline" :loading="authStore.loading"
|
||||||
@click="goToLogin">
|
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
||||||
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
|
@click="goToLogin">
|
||||||
{{ $t('general.back_to_sign_in') }}
|
<UIcon name="mingcute:arrow-left-line"
|
||||||
</button>
|
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
{{ $t('general.back_to_sign_in') }}
|
||||||
{{ $t('general.dont_have_an_account') }}
|
|
||||||
<button variant="link" size="sm" @click="goToRegister"
|
|
||||||
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
|
|
||||||
$t('general.create_account_now') }}
|
|
||||||
</button>
|
</button>
|
||||||
</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
</div>
|
{{ $t('general.dont_have_an_account') }}
|
||||||
</template>
|
<button variant="link" size="sm" @click="goToRegister"
|
||||||
|
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
|
||||||
|
$t('general.create_account_now') }}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<component :is="Empty || 'div'">
|
||||||
<UDrawer v-model:open="showTherms" :overlay="false">
|
<UDrawer v-model:open="showTherms" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="TermsComponent" />
|
<component :is="TermsComponent" />
|
||||||
@@ -21,9 +22,9 @@
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md">
|
<div class="w-full max-w-md">
|
||||||
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
|
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
</UForm>
|
</UForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -120,6 +122,8 @@ import { useValidation } from '@/composable/useValidation'
|
|||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
@@ -181,7 +182,8 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto">
|
||||||
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
||||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -236,7 +238,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</h2>
|
</h2>
|
||||||
<UTable :data="issues" :columns="columns" class="flex-1 dark:text-white! text-dark" />
|
<UTable :data="issues" :columns="columns" class="flex-1 dark:text-white! text-dark" />
|
||||||
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
||||||
<UPagination v-model:page="page" :total="totalItems" />
|
<UPagination v-model:page="page" :total="totalItems" :page-size="10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,4 +256,5 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useValidation } from '@/composable/useValidation'
|
import { useValidation } from '@/composable/useValidation'
|
||||||
import type { FormError } from '@nuxt/ui'
|
import type { FormError } from '@nuxt/ui'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -49,80 +50,84 @@ function validate(): FormError[] {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
<component :is="Empty || 'div'">
|
||||||
<div class="text-center mb-15">
|
<div class="h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div class="text-center mb-15">
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
</div>
|
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
|
||||||
</div>
|
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
|
||||||
|
|
||||||
<template v-if="submitted">
|
|
||||||
<div class="text-center flex flex-col gap-4">
|
|
||||||
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
|
|
||||||
<h2 class="text-xl font-semibold dark:text-white text-black">
|
|
||||||
{{ $t('general.password_updated') }}
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('general.password_updated_description') }}
|
|
||||||
</p>
|
|
||||||
<UButton block @click="goToLogin" class="dark:text-white text-black">
|
|
||||||
{{ $t('general.back_to_sign_in') }}
|
|
||||||
</UButton>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
|
</div>
|
||||||
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
|
|
||||||
<template v-else>
|
<template v-if="submitted">
|
||||||
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
|
<div class="text-center flex flex-col gap-4">
|
||||||
<UAlert v-if="authStore.error" color="error" variant="subtle"
|
<UIcon name="i-heroicons-check-circle" class="w-12 h-12 mx-auto text-green-800" />
|
||||||
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
|
<h2 class="text-xl font-semibold dark:text-white text-black">
|
||||||
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
{{ $t('general.password_updated') }}
|
||||||
@close="authStore.clearError" />
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<UFormField :label="$t('general.new_password')" name="new_password" required
|
{{ $t('general.password_updated_description') }}
|
||||||
class="w-full dark:text-white text-black">
|
</p>
|
||||||
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
|
<UButton block @click="goToLogin" class="dark:text-white text-black">
|
||||||
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
|
|
||||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
|
|
||||||
<template #trailing>
|
|
||||||
<UIcon color="neutral" variant="link" size="sm"
|
|
||||||
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
|
||||||
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
|
|
||||||
:aria-pressed="showNewPassword" aria-controls="new_password"
|
|
||||||
@click="showNewPassword = !showNewPassword" class="mr-2"/>
|
|
||||||
</template>
|
|
||||||
</UInput>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
|
|
||||||
class="w-full dark:text-white text-black">
|
|
||||||
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
|
|
||||||
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
|
|
||||||
class="w-full dark:text-white text-black placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
|
|
||||||
<template #trailing>
|
|
||||||
<UIcon color="neutral" variant="ghost" size="sm"
|
|
||||||
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
|
||||||
@click="showConfirmPassword = !showConfirmPassword" class="mr-2"/>
|
|
||||||
</template>
|
|
||||||
</UInput>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading"
|
|
||||||
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
|
||||||
{{ $t('general.reset_password') }}
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
|
||||||
<button color="neutral" variant="ghost" @click="goToLogin"
|
|
||||||
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
|
|
||||||
<UIcon name="mingcute:arrow-left-line" />
|
|
||||||
{{ $t('general.back_to_sign_in') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</button>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</template>
|
||||||
</template>
|
|
||||||
|
<template v-else>
|
||||||
|
<UForm :validate="validate" @submit="handleReset" class="flex flex-col gap-3">
|
||||||
|
<UAlert v-if="authStore.error" color="error" variant="subtle"
|
||||||
|
icon="i-heroicons-exclamation-triangle" :title="authStore.error"
|
||||||
|
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', variant: 'link' }"
|
||||||
|
@close="authStore.clearError" />
|
||||||
|
|
||||||
|
<UFormField :label="$t('general.new_password')" name="new_password" required
|
||||||
|
class="w-full dark:text-white text-black">
|
||||||
|
<UInput v-model="new_password" :type="showNewPassword ? 'text' : 'password'"
|
||||||
|
:placeholder="$t('general.enter_your_new_password')" :disabled="authStore.loading"
|
||||||
|
class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
|
||||||
|
:ui="{ trailing: 'pe-1' }">
|
||||||
|
<template #trailing>
|
||||||
|
<UIcon color="neutral" variant="link" size="sm"
|
||||||
|
:name="showNewPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||||
|
:aria-label="showNewPassword ? 'Hide password' : 'Show password'"
|
||||||
|
:aria-pressed="showNewPassword" aria-controls="new_password"
|
||||||
|
@click="showNewPassword = !showNewPassword" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField :label="$t('general.confirm_password')" name="confirm_new_password" required
|
||||||
|
class="w-full dark:text-white text-black">
|
||||||
|
<UInput v-model="confirm_new_password" :type="showConfirmPassword ? 'text' : 'password'"
|
||||||
|
:placeholder="$t('general.confirm_your_new_password')" :disabled="authStore.loading"
|
||||||
|
class="w-full dark:text-white text-black placeholder:text-(--placeholder)"
|
||||||
|
:ui="{ trailing: 'pe-1' }">
|
||||||
|
<template #trailing>
|
||||||
|
<UIcon color="neutral" variant="ghost" size="sm"
|
||||||
|
:name="showConfirmPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||||
|
@click="showConfirmPassword = !showConfirmPassword" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton type="submit" block :loading="authStore.loading"
|
||||||
|
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
|
{{ $t('general.reset_password') }}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
||||||
|
<button color="neutral" variant="ghost" @click="goToLogin"
|
||||||
|
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
|
||||||
|
<UIcon name="mingcute:arrow-left-line" />
|
||||||
|
{{ $t('general.back_to_sign_in') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</UForm>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useFetchJson } from '@/composable/useFetchJson'
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
import { i18n } from '@/plugins/02_i18n'
|
import { i18n } from '@/plugins/02_i18n'
|
||||||
|
import Empty from '@/layouts/empty.vue'
|
||||||
|
|
||||||
const { t, te } = useI18n()
|
const { t, te } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -66,81 +67,84 @@ function goToLogin() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<component :is="Empty || 'div'">
|
||||||
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
<div
|
||||||
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
class="min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||||
<div class="w-full max-w-md">
|
<div class="pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-8">
|
<div class="w-full max-w-md">
|
||||||
<div
|
<div class="text-center mb-8">
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
<div
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
|
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
|
||||||
|
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
||||||
|
<template #header>
|
||||||
|
<div class="text-center">
|
||||||
|
<div v-if="verificationInProgress && loading">
|
||||||
|
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ $t('verify_email.verifying') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="success">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
|
||||||
|
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ $t('verify_email.success_title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $t('verify_email.success_message') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
|
||||||
|
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ $t('verify_email.error_title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $t('verify_email.error_message') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="success" class="text-center py-4">
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
|
||||||
|
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="text-center py-4">
|
||||||
|
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
|
||||||
|
class="mb-4" />
|
||||||
|
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ $t('verify_email.already_registered') }}
|
||||||
|
<button variant="link" size="sm" @click="goToLogin"
|
||||||
|
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{
|
||||||
|
$t('general.sign_in')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
|
||||||
<template #header>
|
|
||||||
<div class="text-center">
|
|
||||||
<div v-if="verificationInProgress && loading">
|
|
||||||
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-primary-500 mx-auto mb-4" />
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{{ $t('verify_email.verifying') }}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="success">
|
|
||||||
<div
|
|
||||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4">
|
|
||||||
<UIcon name="i-heroicons-check-circle" class="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{{ $t('verify_email.success_title') }}
|
|
||||||
</h2>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $t('verify_email.success_message') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="error">
|
|
||||||
<div
|
|
||||||
class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4">
|
|
||||||
<UIcon name="i-heroicons-exclamation-circle" class="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{{ $t('verify_email.error_title') }}
|
|
||||||
</h2>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $t('verify_email.error_message') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="success" class="text-center py-4">
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ $t('verify_email.redirect_message') }}</p>
|
|
||||||
<UButton color="primary" @click="goToLogin">{{ $t('verify_email.go_to_login') }}</UButton>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="error" class="text-center py-4">
|
|
||||||
<UAlert :color="'error'" variant="subtle" icon="i-heroicons-exclamation-triangle" :title="error"
|
|
||||||
class="mb-4" />
|
|
||||||
<UButton color="primary" @click="goToLogin" class="cursor-pointer">{{ $t('verify_email.go_to_login') }}
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="verificationInProgress && loading" class="text-center py-4">
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">{{ $t('verify_email.please_wait') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('verify_email.already_registered') }}
|
|
||||||
<button variant="link" size="sm" @click="goToLogin"
|
|
||||||
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in')
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1
|
||||||
github.com/openai/openai-go/v3 v3.28.0
|
github.com/openai/openai-go/v3 v3.28.0
|
||||||
github.com/samber/lo v1.53.0
|
github.com/samber/lo v1.53.0
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -109,6 +109,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M=
|
||||||
|
github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||||
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
|
github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- create routes table
|
-- create routes table
|
||||||
CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
|
CREATE TABLE IF NOT EXISTS b2b_routes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
path VARCHAR(255) NULL,
|
path VARCHAR(255) NULL,
|
||||||
@@ -13,11 +13,11 @@ CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
|
|||||||
|
|
||||||
CONSTRAINT fk_parent
|
CONSTRAINT fk_parent
|
||||||
FOREIGN KEY (parent_id)
|
FOREIGN KEY (parent_id)
|
||||||
REFERENCES b2b_tracker_routes(id)
|
REFERENCES b2b_routes(id)
|
||||||
ON DELETE SET NULL
|
ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
INSERT IGNORE INTO b2b_tracker_routes
|
INSERT IGNORE INTO b2b_routes
|
||||||
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
||||||
VALUES
|
VALUES
|
||||||
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
|
('root', '', '', 'default', '{"trans": "route.root"}', 0, 0, 0),
|
||||||
@@ -30,5 +30,5 @@ VALUES
|
|||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
||||||
DROP TABLE IF EXISTS b2b_tracker_routes;
|
DROP TABLE IF EXISTS b2b_routes;
|
||||||
|
|
||||||
|
|||||||
@@ -86,18 +86,27 @@ CREATE INDEX IF NOT EXISTS idx_customers_deleted_at
|
|||||||
ON b2b_customers (deleted_at);
|
ON b2b_customers (deleted_at);
|
||||||
|
|
||||||
|
|
||||||
-- customer_repo_accesses
|
-- customer_carts
|
||||||
-- CREATE TABLE IF NOT EXISTS b2b_customer_repo_accesses (
|
CREATE TABLE IF NOT EXISTS b2b_customer_carts (
|
||||||
-- user_id BIGINT NOT NULL,
|
cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
-- repo_id BIGINT NOT NULL,
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
-- PRIMARY KEY (user_id, repo_id),
|
name VARCHAR(255) NULL,
|
||||||
-- CONSTRAINT fk_customer_repo_user
|
CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
-- FOREIGN KEY (user_id) REFERENCES b2b_customers(id)
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||||
-- ON DELETE CASCADE,
|
CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (user_id);
|
||||||
-- CONSTRAINT fk_customer_repo_repo
|
|
||||||
-- FOREIGN KEY (repo_id) REFERENCES repository(id)
|
|
||||||
-- ON DELETE CASCADE
|
-- carts_products
|
||||||
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
CREATE TABLE IF NOT EXISTS b2b_carts_products (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
cart_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
product_id INT UNSIGNED NOT NULL,
|
||||||
|
product_attribute_id BIGINT 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
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id);
|
||||||
|
|
||||||
|
|
||||||
-- refresh_tokens
|
-- refresh_tokens
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package categoriesRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UICategoriesRepo interface {
|
|
||||||
GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoriesRepo struct{}
|
|
||||||
|
|
||||||
func New() UICategoriesRepo {
|
|
||||||
return &CategoriesRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *CategoriesRepo) GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error) {
|
|
||||||
var allCategories []model.ScannedCategory
|
|
||||||
|
|
||||||
err := db.DB.Raw(`
|
|
||||||
SELECT
|
|
||||||
ps_category.id_category AS ID,
|
|
||||||
ps_category_lang.name AS name,
|
|
||||||
ps_category.active AS active,
|
|
||||||
ps_category_shop.position AS position,
|
|
||||||
ps_category.id_parent AS id_parent,
|
|
||||||
ps_category.is_root_category AS is_root_category
|
|
||||||
FROM ps_category
|
|
||||||
LEFT JOIN ps_category_lang
|
|
||||||
ON ps_category_lang.id_category = ps_category.id_category
|
|
||||||
AND ps_category_lang.id_shop = ?
|
|
||||||
AND ps_category_lang.id_lang = ?
|
|
||||||
LEFT JOIN ps_category_shop
|
|
||||||
ON ps_category_shop.id_category = ps_category.id_category
|
|
||||||
AND ps_category_shop.id_shop = ?`,
|
|
||||||
id_shop, id_lang, id_shop).
|
|
||||||
Scan(&allCategories).Error
|
|
||||||
|
|
||||||
return allCategories, err
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package langsAndCountriesRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UILangsAndCountriesRepo interface {
|
|
||||||
GetLanguages() ([]model.Language, error)
|
|
||||||
GetCountriesAndCurrencies() ([]model.Country, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LangsAndCountriesRepo struct{}
|
|
||||||
|
|
||||||
func New() UILangsAndCountriesRepo {
|
|
||||||
return &LangsAndCountriesRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *LangsAndCountriesRepo) GetLanguages() ([]model.Language, error) {
|
|
||||||
var languages []model.Language
|
|
||||||
|
|
||||||
err := db.DB.Table("b2b_language").Scan(&languages).Error
|
|
||||||
|
|
||||||
return languages, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *LangsAndCountriesRepo) GetCountriesAndCurrencies() ([]model.Country, error) {
|
|
||||||
var countries []model.Country
|
|
||||||
|
|
||||||
err := db.DB.Table("b2b_countries").
|
|
||||||
Select("b2b_countries.id, b2b_countries.name, b2b_countries.flag, ps_currency.id as id_currency, ps_currency.name as currency_name, ps_currency.iso_code as currency_iso_code").
|
|
||||||
Joins("JOIN ps_currency ON ps_currency.id = b2b_countries.currency").
|
|
||||||
Scan(&countries).Error
|
|
||||||
|
|
||||||
return countries, err
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package listProductsRepo
|
|
||||||
|
|
||||||
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 UIListProductsRepo interface {
|
|
||||||
GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListProductsRepo struct{}
|
|
||||||
|
|
||||||
func New() UIListProductsRepo {
|
|
||||||
return &ListProductsRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
|
||||||
var listing []model.ProductInList
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
// var resultIDs []uint
|
|
||||||
// q := db.DB.
|
|
||||||
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
|
||||||
// // MySQL. It works when followed by `SELECT FOUND_ROWS();`. To learn
|
|
||||||
// // more see: https://mariarawmodel.com/kb/en/found_rows/
|
|
||||||
// // WARN: This might not work on different SQL databases
|
|
||||||
// Select("DISTINCT SQL_CALC_FOUND_ROWS id").
|
|
||||||
// // Debug().
|
|
||||||
// Scopes(view.FromDBViewForDisplay(langID, countryIso)).
|
|
||||||
// Scopes(scopesForFiltersOnDisplay(db.DB, langID, countryIso, filt)).
|
|
||||||
// Scopes(filt.OfCategory(filters.ORDER_FILTER)...).
|
|
||||||
// Limit(p.Limit()).
|
|
||||||
// Offset(p.Offset())
|
|
||||||
|
|
||||||
err := db.DB.Raw(`
|
|
||||||
SELECT
|
|
||||||
ps_product.id_product AS ID,
|
|
||||||
ps_product_lang.name AS name,
|
|
||||||
ps_product.active AS active,
|
|
||||||
ps_product_lang.link_rewrite AS link_rewrite,
|
|
||||||
COALESCE (
|
|
||||||
ps_image_shop.id_image, any_image.id_image
|
|
||||||
) AS id_image
|
|
||||||
FROM ps_product
|
|
||||||
LEFT JOIN ps_product_lang
|
|
||||||
ON ps_product_lang.id_product = ps_product.id_product
|
|
||||||
AND ps_product_lang.id_shop = ?
|
|
||||||
AND ps_product_lang.id_lang = ?
|
|
||||||
LEFT JOIN ps_image_shop
|
|
||||||
ON ps_image_shop.id_product = ps_product.id_product
|
|
||||||
AND ps_image_shop.id_shop = ?
|
|
||||||
AND ps_image_shop.cover = 1
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id_product, MIN(id_image) AS id_image
|
|
||||||
FROM ps_image
|
|
||||||
GROUP BY id_product
|
|
||||||
) any_image
|
|
||||||
ON ps_product.id_product = any_image.id_product
|
|
||||||
LIMIT ? OFFSET ?`,
|
|
||||||
id_shop, id_lang, id_shop, p.Limit(), p.Offset()).
|
|
||||||
Scan(&listing).Error
|
|
||||||
if err != nil {
|
|
||||||
return find.Found[model.ProductInList]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.DB.Raw(`
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM ps_product`).
|
|
||||||
Scan(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return find.Found[model.ProductInList]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return find.Found[model.ProductInList]{
|
|
||||||
Items: listing,
|
|
||||||
Count: uint(total),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package productDescriptionRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UIProductDescriptionRepo interface {
|
|
||||||
GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error)
|
|
||||||
CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error
|
|
||||||
UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductDescriptionRepo struct{}
|
|
||||||
|
|
||||||
func New() UIProductDescriptionRepo {
|
|
||||||
return &ProductDescriptionRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We assume that any user has access to all product descriptions
|
|
||||||
func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
|
||||||
var ProductDescription model.ProductDescription
|
|
||||||
|
|
||||||
err := db.DB.
|
|
||||||
Table("ps_product_lang").
|
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
||||||
First(&ProductDescription).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProductDescription, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it doesn't exist, returns an error.
|
|
||||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productShopID uint, productLangID uint) error {
|
|
||||||
record := model.ProductDescription{
|
|
||||||
ProductID: productID,
|
|
||||||
ShopID: productShopID,
|
|
||||||
LangID: productLangID,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.DB.
|
|
||||||
Table("ps_product_lang").
|
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
||||||
FirstOrCreate(&record).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProductDescriptionRepo) UpdateFields(productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
updatesIface := make(map[string]interface{}, len(updates))
|
|
||||||
for k, v := range updates {
|
|
||||||
updatesIface[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.DB.
|
|
||||||
Table("ps_product_lang").
|
|
||||||
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
||||||
Updates(updatesIface).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ tasks:
|
|||||||
dump:
|
dump:
|
||||||
desc: Dump remote Gitea DB to file
|
desc: Dump remote Gitea DB to file
|
||||||
aliases: [dump]
|
aliases: [dump]
|
||||||
# silent: true
|
silent: true
|
||||||
vars:
|
vars:
|
||||||
TEMP_DB_NAME: "temp_db_copy"
|
TEMP_DB_NAME: "temp_db_copy"
|
||||||
SERVER: https://file.ma-al.pl
|
SERVER: https://file.ma-al.pl
|
||||||
|
|||||||
Reference in New Issue
Block a user