Compare commits
10 Commits
1bab7f642f
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ffdff4e28 | |||
| e34d686244 | |||
| 12f6249721 | |||
| 4386337c4a | |||
| d83bee2e34 | |||
| 7bd1c5a9c9 | |||
| 0e9df17eab | |||
| faa990ca9b | |||
| 3c6fa077a0 | |||
| fef83eb46b |
4
.env
4
.env
@@ -48,10 +48,6 @@ EMAIL_FROM=test@ma-al.com
|
|||||||
EMAIL_FROM_NAME=Gitea Manager
|
EMAIL_FROM_NAME=Gitea Manager
|
||||||
EMAIL_ADMIN=goc_marek@ma-al.pl
|
EMAIL_ADMIN=goc_marek@ma-al.pl
|
||||||
|
|
||||||
# STORAGE
|
|
||||||
STORAGE_ROOT=./storage
|
|
||||||
|
|
||||||
|
|
||||||
I18N_LANGS=en,pl,cs
|
I18N_LANGS=en,pl,cs
|
||||||
|
|
||||||
PDF_SERVER_URL=http://localhost:8000
|
PDF_SERVER_URL=http://localhost:8000
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,5 +6,3 @@ i18n/*.json
|
|||||||
*_templ.go
|
*_templ.go
|
||||||
tmp/main
|
tmp/main
|
||||||
test.go
|
test.go
|
||||||
storage/*
|
|
||||||
!storage/.gitkeep
|
|
||||||
@@ -2,10 +2,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,8 +24,7 @@ type Config struct {
|
|||||||
GoogleTranslate GoogleTranslateConfig
|
GoogleTranslate GoogleTranslateConfig
|
||||||
Image ImageConfig
|
Image ImageConfig
|
||||||
Cors CorsConfig
|
Cors CorsConfig
|
||||||
MeiliSearch MeiliSearchConfig
|
MailiSearch MeiliSearchConfig
|
||||||
Storage StorageConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type I18n struct {
|
type I18n struct {
|
||||||
@@ -98,10 +95,6 @@ type EmailConfig struct {
|
|||||||
Enabled bool `env:"EMAIL_ENABLED,false"`
|
Enabled bool `env:"EMAIL_ENABLED,false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageConfig struct {
|
|
||||||
RootFolder string `env:"STORAGE_ROOT"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PdfPrinter struct {
|
type PdfPrinter struct {
|
||||||
ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"`
|
ServerUrl string `env:"PDF_SERVER_URL,http://localhost:8000"`
|
||||||
}
|
}
|
||||||
@@ -162,7 +155,7 @@ func load() *Config {
|
|||||||
|
|
||||||
err = loadEnv(&cfg.OAuth.Google)
|
err = loadEnv(&cfg.OAuth.Google)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for oauth google : ", err.Error(), "")
|
slog.Error("not possible to load env variables for outh google : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.App)
|
err = loadEnv(&cfg.App)
|
||||||
@@ -177,12 +170,12 @@ func load() *Config {
|
|||||||
|
|
||||||
err = loadEnv(&cfg.I18n)
|
err = loadEnv(&cfg.I18n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for i18n : ", err.Error(), "")
|
slog.Error("not possible to load env variables for email : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.Pdf)
|
err = loadEnv(&cfg.Pdf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for pdf : ", err.Error(), "")
|
slog.Error("not possible to load env variables for email : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.GoogleTranslate)
|
err = loadEnv(&cfg.GoogleTranslate)
|
||||||
@@ -192,25 +185,19 @@ func load() *Config {
|
|||||||
|
|
||||||
err = loadEnv(&cfg.Image)
|
err = loadEnv(&cfg.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for image : ", err.Error(), "")
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.Cors)
|
err = loadEnv(&cfg.Cors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for cors : ", err.Error(), "")
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.MeiliSearch)
|
err = loadEnv(&cfg.MailiSearch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("not possible to load env variables for meili search : ", err.Error(), "")
|
slog.Error("not possible to load env variables for google translate : ", err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = loadEnv(&cfg.Storage)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("not possible to load env variables for storage : ", err.Error(), "")
|
|
||||||
}
|
|
||||||
cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder)
|
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,22 +308,6 @@ func setValue(field reflect.Value, val string, key string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveRelativePath(relativePath string) string {
|
|
||||||
// get working directory (where program was started)
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert to absolute path
|
|
||||||
absPath := relativePath
|
|
||||||
if !filepath.IsAbs(absPath) {
|
|
||||||
absPath = filepath.Join(wd, absPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Clean(absPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseEnvTag(tag string) (key string, def *string) {
|
func parseEnvTag(tag string) (key string, def *string) {
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
package restricted
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StorageHandler struct {
|
|
||||||
storageService *storageService.StorageService
|
|
||||||
config *config.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStorageHandler() *StorageHandler {
|
|
||||||
return &StorageHandler{
|
|
||||||
storageService: storageService.New(),
|
|
||||||
config: config.Get(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func StorageHandlerRoutes(r fiber.Router) fiber.Router {
|
|
||||||
handler := NewStorageHandler()
|
|
||||||
|
|
||||||
r.Get("/list-content/*", handler.ListContent)
|
|
||||||
r.Get("/download-file/*", handler.DownloadFile)
|
|
||||||
|
|
||||||
r.Get("/move/*", handler.Move)
|
|
||||||
r.Get("/copy/*", handler.Copy)
|
|
||||||
r.Post("/upload-file/*", handler.UploadFile)
|
|
||||||
r.Get("/create-folder/*", handler.CreateFolder)
|
|
||||||
r.Delete("/delete-file/*", handler.DeleteFile)
|
|
||||||
r.Delete("/delete-folder/*", handler.DeleteFolder)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) Move(c fiber.Ctx) error {
|
|
||||||
src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.Move(src_abs_path, dest_abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) Copy(c fiber.Ctx) error {
|
|
||||||
src_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
dest_abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Query("dest_path"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.Copy(src_abs_path, dest_abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// accepted path looks like e.g. "/folder1/" or "folder1"
|
|
||||||
func (h *StorageHandler) ListContent(c fiber.Ctx) error {
|
|
||||||
// relative path defaults to root directory
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
entries_in_list, err := h.storageService.ListContent(abs_path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(entries_in_list, 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Attachment(filename)
|
|
||||||
c.Set("Content-Length", strconv.FormatInt(filesize, 10))
|
|
||||||
c.Set("Content-Type", "application/octet-stream")
|
|
||||||
return c.SendStream(f, int(filesize))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) UploadFile(c fiber.Ctx) error {
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := c.FormFile("document")
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrMissingFileFieldDocument)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrMissingFileFieldDocument)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.UploadFile(c, abs_path, f)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) CreateFolder(c fiber.Ctx) error {
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.CreateFolder(abs_path, c.Query("name"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) DeleteFile(c fiber.Ctx) error {
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.DeleteFile(abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *StorageHandler) DeleteFolder(c fiber.Ctx) error {
|
|
||||||
abs_path, err := h.storageService.AbsPath(h.config.Storage.RootFolder, c.Params("*"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.storageService.DeleteFolder(abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
@@ -115,10 +115,6 @@ func (s *Server) Setup() error {
|
|||||||
carts := s.restricted.Group("/carts")
|
carts := s.restricted.Group("/carts")
|
||||||
restricted.CartsHandlerRoutes(carts)
|
restricted.CartsHandlerRoutes(carts)
|
||||||
|
|
||||||
// storage (restricted)
|
|
||||||
storage := s.restricted.Group("/storage")
|
|
||||||
restricted.StorageHandlerRoutes(storage)
|
|
||||||
|
|
||||||
s.api.All("*", func(c fiber.Ctx) error {
|
s.api.All("*", func(c fiber.Ctx) error {
|
||||||
return c.SendStatus(fiber.StatusNotFound)
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type EntryInList struct {
|
|
||||||
Name string
|
|
||||||
IsFolder bool
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ 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"`
|
||||||
|
|
||||||
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
|
ExistsInDatabse bool `gorm:"-" json:"exists_in_database"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductRow struct {
|
type ProductRow struct {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (repo *ListRepo) ListProducts(id_lang uint, p find.Paging, filt *filters.Fi
|
|||||||
ps.id_product AS product_id,
|
ps.id_product AS product_id,
|
||||||
pl.name AS name,
|
pl.name AS name,
|
||||||
pl.link_rewrite AS link_rewrite,
|
pl.link_rewrite AS link_rewrite,
|
||||||
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
CONCAT(?, ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
|
||||||
cl.name AS category_name,
|
cl.name AS category_name,
|
||||||
p.reference AS reference,
|
p.reference AS reference,
|
||||||
COALESCE(v.variants_number, 0) AS variants_number,
|
COALESCE(v.variants_number, 0) AS variants_number,
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
|||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// handle "not found" case only
|
// handle "not found" case only
|
||||||
ProductDescription.ExistsInDatabase = false
|
ProductDescription.ExistsInDatabse = false
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
return nil, fmt.Errorf("database error: %w", err)
|
||||||
} else {
|
} else {
|
||||||
ProductDescription.ExistsInDatabase = true
|
ProductDescription.ExistsInDatabse = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ProductDescription, nil
|
return &ProductDescription, nil
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ func New() UISearchRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) {
|
func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) {
|
||||||
url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MeiliSearch.ServerURL, index)
|
url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MailiSearch.ServerURL, index)
|
||||||
return r.doRequest(http.MethodPost, url, body)
|
return r.doRequest(http.MethodPost, url, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) {
|
func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) {
|
||||||
url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MeiliSearch.ServerURL, index)
|
url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MailiSearch.ServerURL, index)
|
||||||
return r.doRequest(http.MethodGet, url, nil)
|
return r.doRequest(http.MethodGet, url, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
if r.cfg.MeiliSearch.ApiKey != "" {
|
if r.cfg.MailiSearch.ApiKey != "" {
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MailiSearch.ApiKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
package storageRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UIStorageRepo interface {
|
|
||||||
EntryInfo(abs_path string) (os.FileInfo, error)
|
|
||||||
ListContent(abs_path string) (*[]model.EntryInList, error)
|
|
||||||
Move(src_abs_path string, dest_abs_path string) error
|
|
||||||
Copy(src_abs_path string, dest_abs_path string) error
|
|
||||||
OpenFile(abs_path string) (*os.File, error)
|
|
||||||
UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error
|
|
||||||
CreateFolder(abs_path string) error
|
|
||||||
DeleteFile(abs_path string) error
|
|
||||||
DeleteFolder(abs_path string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageRepo struct{}
|
|
||||||
|
|
||||||
func New() UIStorageRepo {
|
|
||||||
return &StorageRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) EntryInfo(abs_path string) (os.FileInfo, error) {
|
|
||||||
return os.Stat(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) ListContent(abs_path string) (*[]model.EntryInList, error) {
|
|
||||||
entries, err := os.ReadDir(abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries_in_list []model.EntryInList
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
var next_entry_in_list model.EntryInList
|
|
||||||
next_entry_in_list.Name = entry.Name()
|
|
||||||
next_entry_in_list.IsFolder = entry.IsDir()
|
|
||||||
|
|
||||||
entries_in_list = append(entries_in_list, next_entry_in_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &entries_in_list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) Move(src_abs_path string, dest_abs_path string) error {
|
|
||||||
return os.Rename(src_abs_path, dest_abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) Copy(src_abs_path string, dest_abs_path string) error {
|
|
||||||
in, err := os.Open(src_abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
info, err := in.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.OpenFile(dest_abs_path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) OpenFile(abs_path string) (*os.File, error) {
|
|
||||||
return os.Open(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error {
|
|
||||||
return c.SaveFile(f, abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) CreateFolder(abs_path string) error {
|
|
||||||
return os.Mkdir(abs_path, 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) DeleteFile(abs_path string) error {
|
|
||||||
return os.Remove(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StorageRepo) DeleteFolder(abs_path string) error {
|
|
||||||
return os.RemoveAll(abs_path)
|
|
||||||
}
|
|
||||||
@@ -27,8 +27,8 @@ type MeiliService struct {
|
|||||||
func New() *MeiliService {
|
func New() *MeiliService {
|
||||||
|
|
||||||
client := meilisearch.New(
|
client := meilisearch.New(
|
||||||
config.Get().MeiliSearch.ServerURL,
|
config.Get().MailiSearch.ServerURL,
|
||||||
meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey),
|
meilisearch.WithAPIKey(config.Get().MailiSearch.ApiKey),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &MeiliService{
|
return &MeiliService{
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
package storageService
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/repos/storageRepo"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StorageService struct {
|
|
||||||
storageRepo storageRepo.UIStorageRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *StorageService {
|
|
||||||
return &StorageService{
|
|
||||||
storageRepo: storageRepo.New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) ListContent(abs_path string) (*[]model.EntryInList, error) {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return nil, responseErrors.ErrFolderDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
entries_in_list, err := s.storageRepo.ListContent(abs_path)
|
|
||||||
return entries_in_list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) DownloadFilePrep(abs_path string) (*os.File, string, int64, error) {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || info.IsDir() {
|
|
||||||
return nil, "", 0, responseErrors.ErrFileDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := s.storageRepo.OpenFile(abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, filepath.Base(abs_path), info.Size(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) Move(src_abs_path string, dest_abs_path string) error {
|
|
||||||
_, err := s.storageRepo.EntryInfo(src_abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return responseErrors.ErrFileDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.storageRepo.EntryInfo(dest_abs_path)
|
|
||||||
if err == nil {
|
|
||||||
return responseErrors.ErrNameTaken
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
return s.storageRepo.Move(src_abs_path, dest_abs_path)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (s *StorageService) Copy(src_abs_path string, dest_abs_path string) error {
|
|
||||||
_, err := s.storageRepo.EntryInfo(src_abs_path)
|
|
||||||
if err != nil {
|
|
||||||
return responseErrors.ErrFileDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.storageRepo.EntryInfo(dest_abs_path)
|
|
||||||
if err == nil {
|
|
||||||
return responseErrors.ErrNameTaken
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
return s.storageRepo.Copy(src_abs_path, dest_abs_path)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) UploadFile(c fiber.Ctx, abs_path string, f *multipart.FileHeader) error {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return responseErrors.ErrFolderDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
name := f.Filename
|
|
||||||
if name == "" || name == "." || name == ".." || filepath.Base(name) != name {
|
|
||||||
return responseErrors.ErrBadAttribute
|
|
||||||
}
|
|
||||||
abs_file_path, err := s.AbsPath(abs_path, name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if abs_file_path == abs_path {
|
|
||||||
return responseErrors.ErrBadAttribute
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err = s.storageRepo.EntryInfo(abs_file_path)
|
|
||||||
if err == nil {
|
|
||||||
return responseErrors.ErrNameTaken
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
return s.storageRepo.UploadFile(c, abs_file_path, f)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) CreateFolder(abs_path string, name string) error {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return responseErrors.ErrFolderDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "" || name == "." || name == ".." || filepath.Base(name) != name {
|
|
||||||
return responseErrors.ErrBadAttribute
|
|
||||||
}
|
|
||||||
abs_folder_path, err := s.AbsPath(abs_path, name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if abs_folder_path == abs_path {
|
|
||||||
return responseErrors.ErrBadAttribute
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err = s.storageRepo.EntryInfo(abs_folder_path)
|
|
||||||
if err == nil {
|
|
||||||
return responseErrors.ErrNameTaken
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
return s.storageRepo.CreateFolder(abs_folder_path)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) DeleteFile(abs_path string) error {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || info.IsDir() {
|
|
||||||
return responseErrors.ErrFileDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.storageRepo.DeleteFile(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StorageService) DeleteFolder(abs_path string) error {
|
|
||||||
info, err := s.storageRepo.EntryInfo(abs_path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return responseErrors.ErrFolderDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.storageRepo.DeleteFolder(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbsPath extracts an absolute path and validates it
|
|
||||||
func (s *StorageService) AbsPath(root string, relativePath string) (string, error) {
|
|
||||||
clean_name := filepath.Clean(relativePath)
|
|
||||||
full_path := filepath.Join(root, clean_name)
|
|
||||||
|
|
||||||
if full_path != root && !strings.HasPrefix(full_path, root+string(os.PathSeparator)) {
|
|
||||||
return "", responseErrors.ErrAccessDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
return full_path, nil
|
|
||||||
}
|
|
||||||
@@ -59,13 +59,6 @@ var (
|
|||||||
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
|
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
|
||||||
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
|
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
|
||||||
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
|
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
|
||||||
|
|
||||||
// Typed errors for storage
|
|
||||||
ErrAccessDenied = errors.New("access denied!")
|
|
||||||
ErrFolderDoesNotExist = errors.New("folder does not exist")
|
|
||||||
ErrFileDoesNotExist = errors.New("file does not exist")
|
|
||||||
ErrNameTaken = errors.New("name taken")
|
|
||||||
ErrMissingFileFieldDocument = errors.New("missing file field 'document'")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents an error with HTTP status code
|
// Error represents an error with HTTP status code
|
||||||
@@ -169,17 +162,6 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||||
return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
|
return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
|
||||||
|
|
||||||
case errors.Is(err, ErrAccessDenied):
|
|
||||||
return i18n.T_(c, "error.access_denied")
|
|
||||||
case errors.Is(err, ErrFolderDoesNotExist):
|
|
||||||
return i18n.T_(c, "error.folder_does_not_exist")
|
|
||||||
case errors.Is(err, ErrFileDoesNotExist):
|
|
||||||
return i18n.T_(c, "error.file_does_not_exist")
|
|
||||||
case errors.Is(err, ErrNameTaken):
|
|
||||||
return i18n.T_(c, "error.name_taken")
|
|
||||||
case errors.Is(err, ErrMissingFileFieldDocument):
|
|
||||||
return i18n.T_(c, "error.missing_file_field_document")
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return i18n.T_(c, "error.err_internal_server_error")
|
return i18n.T_(c, "error.err_internal_server_error")
|
||||||
}
|
}
|
||||||
@@ -221,12 +203,7 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrRootNeverReached),
|
errors.Is(err, ErrRootNeverReached),
|
||||||
errors.Is(err, ErrMaxAmtOfCartsReached),
|
errors.Is(err, ErrMaxAmtOfCartsReached),
|
||||||
errors.Is(err, ErrUserHasNoSuchCart),
|
errors.Is(err, ErrUserHasNoSuchCart),
|
||||||
errors.Is(err, ErrProductOrItsVariationDoesNotExist),
|
errors.Is(err, ErrProductOrItsVariationDoesNotExist):
|
||||||
errors.Is(err, ErrAccessDenied),
|
|
||||||
errors.Is(err, ErrFolderDoesNotExist),
|
|
||||||
errors.Is(err, ErrFileDoesNotExist),
|
|
||||||
errors.Is(err, ErrNameTaken),
|
|
||||||
errors.Is(err, ErrMissingFileFieldDocument):
|
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
|
|||||||
6
bo/components.d.ts
vendored
6
bo/components.d.ts
vendored
@@ -11,16 +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']
|
||||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||||
|
CartSelector: typeof import('./src/components/customer/CartSelector.vue')['default']
|
||||||
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
|
CategoryMenu: typeof import('./src/components/inner/categoryMenu.vue')['default']
|
||||||
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
||||||
|
copy: typeof import('./src/components/inner/categoryMenu copy.vue')['default']
|
||||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
|
Cs_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']
|
||||||
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']
|
||||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||||
|
PageCheckout: typeof import('./src/components/customer/PageCheckout.vue')['default']
|
||||||
PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
|
PageOrders: typeof import('./src/components/customer/PageOrders.vue')['default']
|
||||||
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
|
PageProduct: typeof import('./src/components/customer/PageProduct.vue')['default']
|
||||||
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
|
PageProducts: typeof import('./src/components/admin/PageProducts.vue')['default']
|
||||||
@@ -31,6 +36,7 @@ declare module 'vue' {
|
|||||||
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']
|
||||||
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
||||||
|
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
|
||||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|||||||
@@ -58,12 +58,11 @@ await getTopMenu()
|
|||||||
<!-- px-4 sm:px-6 lg:px-8 -->
|
<!-- px-4 sm:px-6 lg:px-8 -->
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex items-center justify-between h-14">
|
<div class="flex items-center justify-between h-14">
|
||||||
<!-- 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>
|
||||||
|
|
||||||
<UNavigationMenu :type="'trigger'" :ui="{
|
<UNavigationMenu :type="'trigger'" :ui="{
|
||||||
@@ -71,9 +70,7 @@ await getTopMenu()
|
|||||||
list: 'gap-4'
|
list: 'gap-4'
|
||||||
}" :items="menuItems" class="w-full"></UNavigationMenu>
|
}" :items="menuItems" class="w-full"></UNavigationMenu>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
<!-- Theme Switcher -->
|
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<!-- Logout Button (only when authenticated) -->
|
<!-- Logout Button (only when authenticated) -->
|
||||||
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||||
|
|||||||
@@ -11,18 +11,14 @@ const authStore = useAuthStore()
|
|||||||
class="fixed top-0 left-0 right-0 z-50 bg-(--main-light)/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
class="fixed top-0 left-0 right-0 z-50 bg-(--main-light)/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
<div class="container px-4 sm:px-6 lg:px-8">
|
<div class="container px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex items-center justify-between h-14">
|
<div class="flex items-center justify-between h-14">
|
||||||
<!-- 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="carbon:ibm-webmethods-b2b-integration" 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">B2B</span>
|
<span class="font-semibold text-gray-900 dark:text-white">B2B</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<!-- Right Side Actions -->
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
<!-- Theme Switcher -->
|
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<suspense>
|
<suspense>
|
||||||
<component :is="Default || 'div'">
|
<component :is="Default || 'div'">
|
||||||
<div class="flex gap-10">
|
<!-- <div class="w-64 h-128">
|
||||||
<CategoryMenu />
|
<CategoryMenu />
|
||||||
<div class="w-full flex flex-col items-center gap-4">
|
</div> -->
|
||||||
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
|
|
||||||
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
|
<CategoryMenuListing />
|
||||||
|
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||||
|
<template #expanded="{ row }">
|
||||||
|
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||||
|
thead: 'hidden'
|
||||||
|
}" />
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
|
||||||
|
<div 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 Code</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"
|
||||||
|
@click="goToProduct(product.product_id)">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<img :src="product.image_link" 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.reference }}</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.link_rewrite }}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
@@ -18,20 +77,27 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
|||||||
import Default from '@/layouts/default.vue'
|
import Default from '@/layouts/default.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
|
||||||
import type { Product } from '@/types/product'
|
|
||||||
|
interface Product {
|
||||||
|
reference: number
|
||||||
|
product_id: number
|
||||||
|
name: string
|
||||||
|
image_link: string
|
||||||
|
link_rewrite: string
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const perPage = ref(15)
|
|
||||||
const page = computed({
|
const page = computed({
|
||||||
get: () => Number(route.query.p) || 1,
|
get: () => Number(route.query.page) || 1,
|
||||||
set: (val: number) => {
|
set: (val: number) => {
|
||||||
router.push({
|
router.push({
|
||||||
query: {
|
query: {
|
||||||
...route.query,
|
...route.query,
|
||||||
p: val
|
page: val
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,6 +133,7 @@ const sortField = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const perPage = ref(15)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
||||||
interface ApiResponse {
|
interface ApiResponse {
|
||||||
@@ -127,25 +194,17 @@ async function fetchProductList() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
Object.entries(route.query).forEach(([key, value]) => {
|
Object.entries(route.query).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) return
|
if (value) params.append(key, String(value))
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach(v => params.append(key, String(v)))
|
|
||||||
} else {
|
|
||||||
params.append(key, String(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (route.params.category_id)
|
const url = `/api/v1/restricted/list/list-products?${params}`
|
||||||
params.append('category_id', String(route.params.category_id))
|
|
||||||
|
|
||||||
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}`
|
|
||||||
try {
|
try {
|
||||||
const response = await useFetchJson<ApiResponse>(url)
|
const response = await useFetchJson<ApiResponse>(url)
|
||||||
productsList.value = response.items || []
|
productsList.value = response.items || []
|
||||||
total.value = Number(response.count) || 0
|
total.value = response.count || 0
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -153,13 +212,19 @@ async function fetchProductList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToProduct(productId: number) {
|
function goToProduct(productId: number, imageLink: string) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'customer-product-details',
|
name: 'product-detail',
|
||||||
params: { product_id: productId }
|
params: { id: productId },
|
||||||
|
query: { image: imageLink }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedCount = ref({
|
||||||
|
product_id: null as number | null,
|
||||||
|
count: 0
|
||||||
|
})
|
||||||
|
|
||||||
function getIcon(name: string) {
|
function getIcon(name: string) {
|
||||||
if (sortField.value[0] === name) {
|
if (sortField.value[0] === name) {
|
||||||
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
||||||
@@ -174,6 +239,24 @@ const UButton = resolveComponent('UButton')
|
|||||||
const UIcon = resolveComponent('UIcon')
|
const UIcon = resolveComponent('UIcon')
|
||||||
|
|
||||||
const columns: TableColumn<Product>[] = [
|
const columns: TableColumn<Product>[] = [
|
||||||
|
{
|
||||||
|
id: 'expand',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
h(UButton, {
|
||||||
|
color: 'neutral',
|
||||||
|
variant: 'ghost',
|
||||||
|
icon: 'i-lucide-chevron-down',
|
||||||
|
square: true,
|
||||||
|
'aria-label': 'Expand',
|
||||||
|
ui: {
|
||||||
|
leadingIcon: [
|
||||||
|
'transition-transform',
|
||||||
|
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onClick: () => row.toggleExpanded()
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'product_id',
|
accessorKey: 'product_id',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
@@ -200,8 +283,13 @@ const columns: TableColumn<Product>[] = [
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
// header: '#',
|
cell: ({ row }) => h('span', {
|
||||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
class: 'cursor-pointer text-blue-500 hover:underline',
|
||||||
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToProduct(row.original.product_id, row.original.image_link)
|
||||||
|
}
|
||||||
|
}, `#${row.getValue('product_id') as number}`)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'image_link',
|
accessorKey: 'image_link',
|
||||||
@@ -239,7 +327,13 @@ const columns: TableColumn<Product>[] = [
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
cell: ({ row }) => row.getValue('name') as string,
|
cell: ({ row }) => h('span', {
|
||||||
|
class: 'cursor-pointer text-blue-500 hover:underline',
|
||||||
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToProduct(row.original.product_id, row.original.image_link)
|
||||||
|
}
|
||||||
|
}, row.getValue('name') as string),
|
||||||
filterFn: (row, columnId, value) => {
|
filterFn: (row, columnId, value) => {
|
||||||
const name = row.getValue(columnId) as string
|
const name = row.getValue(columnId) as string
|
||||||
return name.toLowerCase().includes(value.toLowerCase())
|
return name.toLowerCase().includes(value.toLowerCase())
|
||||||
@@ -264,17 +358,120 @@ const columns: TableColumn<Product>[] = [
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => row.getValue('quantity') as number
|
cell: ({ row }) => row.getValue('quantity') as number
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'count',
|
||||||
|
header: 'Count',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return h(UInputNumber, {
|
||||||
|
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||||
|
'onUpdate:modelValue': (val: number) => {
|
||||||
|
if (val)
|
||||||
|
selectedCount.value = {
|
||||||
|
product_id: row.original.product_id,
|
||||||
|
count: val
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedCount.value = {
|
||||||
|
product_id: null,
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
max: row.original.quantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'count',
|
accessorKey: 'count',
|
||||||
header: '',
|
header: '',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return h(UButton, {
|
return h(UButton, {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
goToProduct(row.original.product_id)
|
console.log('Clicked', row.original)
|
||||||
},
|
},
|
||||||
color: 'primary',
|
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||||
|
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||||
variant: 'solid'
|
variant: 'solid'
|
||||||
}, () => 'Show product')
|
}, 'Add to cart')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const columnsChild: TableColumn<Product>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'product_id',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => h('span', {
|
||||||
|
class: 'cursor-pointer text-blue-500 hover:underline',
|
||||||
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToProduct(row.original.product_id, row.original.image_link)
|
||||||
|
}
|
||||||
|
}, `#${row.getValue('product_id') as number}`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'image_link',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return h('img', {
|
||||||
|
src: row.getValue('image_link') as string,
|
||||||
|
style: 'width:40px;height:40px;object-fit:cover;'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => h('span', {
|
||||||
|
class: 'cursor-pointer text-blue-500 hover:underline',
|
||||||
|
onClick: (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToProduct(row.original.product_id, row.original.image_link)
|
||||||
|
}
|
||||||
|
}, row.getValue('name') as string)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'quantity',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => row.getValue('quantity') as number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'count',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return h(UInputNumber, {
|
||||||
|
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||||
|
'onUpdate:modelValue': (val: number) => {
|
||||||
|
if (val)
|
||||||
|
selectedCount.value = {
|
||||||
|
product_id: row.original.product_id,
|
||||||
|
count: val
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedCount.value = {
|
||||||
|
product_id: null,
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
max: row.original.quantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'count',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return h(UButton, {
|
||||||
|
onClick: () => {
|
||||||
|
console.log('Clicked', row.original)
|
||||||
|
},
|
||||||
|
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||||
|
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||||
|
variant: 'solid'
|
||||||
|
}, 'Add to cart')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="Default || 'div'">
|
<component :is="Default || 'div'">
|
||||||
<div class="container my-10 mx-auto ">
|
<div class="container my-10 mx-auto ">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
||||||
<div class="flex items-end gap-3">
|
<div class="flex items-end gap-3">
|
||||||
@@ -101,7 +100,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<p ref="usageRef" v-html="productStore.productDescription.usage"
|
<p ref="usageRef" v-html="productStore.productDescription.usage"
|
||||||
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
|
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeTab === 'description'"
|
<div v-if="activeTab === 'description'"
|
||||||
@@ -151,7 +149,10 @@ const selectedLanguage = ref('en')
|
|||||||
const currentLangId = ref(2)
|
const currentLangId = ref(2)
|
||||||
const productID = ref<number>(0)
|
const productID = ref<number>(0)
|
||||||
|
|
||||||
// Watch for language changes and refetch product description
|
const imageUrl = computed(() => {
|
||||||
|
return route.query.image ? String(route.query.image) : ''
|
||||||
|
})
|
||||||
|
|
||||||
watch(selectedLanguage, async (newLang: string) => {
|
watch(selectedLanguage, async (newLang: string) => {
|
||||||
if (productID.value) {
|
if (productID.value) {
|
||||||
await fetchForLanguage(newLang)
|
await fetchForLanguage(newLang)
|
||||||
@@ -197,10 +198,14 @@ const originalDescription = ref('')
|
|||||||
const originalUsage = ref('')
|
const originalUsage = ref('')
|
||||||
|
|
||||||
const saveDescription = async () => {
|
const saveDescription = async () => {
|
||||||
descriptionEdit.disableEdit()
|
if (descriptionRef.value) {
|
||||||
await productStore.saveProductDescription(productID.value)
|
productStore.productDescription.description = descriptionRef.value.innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descriptionEdit.disableEdit()
|
||||||
|
|
||||||
|
await productStore.saveProductDescription(productID.value, currentLangId.value)
|
||||||
|
}
|
||||||
const cancelDescriptionEdit = () => {
|
const cancelDescriptionEdit = () => {
|
||||||
if (descriptionRef.value) {
|
if (descriptionRef.value) {
|
||||||
descriptionRef.value.innerHTML = originalDescription.value
|
descriptionRef.value.innerHTML = originalDescription.value
|
||||||
@@ -224,9 +229,14 @@ const enableEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveText = () => {
|
const saveText = () => {
|
||||||
|
if (usageRef.value) {
|
||||||
|
productStore.productDescription.usage = usageRef.value.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
usageEdit.disableEdit()
|
usageEdit.disableEdit()
|
||||||
isEditing.value = false
|
isEditing.value = false
|
||||||
productStore.saveProductDescription(productID.value)
|
|
||||||
|
productStore.saveProductDescription(productID.value, currentLangId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEdit = () => {
|
const cancelEdit = () => {
|
||||||
@@ -236,14 +246,5 @@ const cancelEdit = () => {
|
|||||||
usageEdit.disableEdit()
|
usageEdit.disableEdit()
|
||||||
isEditing.value = false
|
isEditing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.images {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 70px;
|
|
||||||
margin: 20px 0 20px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
237
bo/src/components/admin/ProductsView.vue
Normal file
237
bo/src/components/admin/ProductsView.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useProductStore, type Product } from '@/stores/product'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const productStore = useProductStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const searchName = ref('')
|
||||||
|
const searchCode = ref('')
|
||||||
|
const priceFromFilter = ref<number | null>(null)
|
||||||
|
const priceToFilter = ref<number | null>(null)
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 5
|
||||||
|
|
||||||
|
// Fetch products on mount
|
||||||
|
// onMounted(() => {
|
||||||
|
// productStore.getProductDescription(langID: , productID.value)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Filtered products
|
||||||
|
// const filteredProducts = computed(() => {
|
||||||
|
// console.log(productStore.products);
|
||||||
|
|
||||||
|
// return productStore.products.filter(product => {
|
||||||
|
// const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
|
||||||
|
// const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
|
||||||
|
// const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
|
||||||
|
// const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
|
||||||
|
|
||||||
|
// return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const totalItems = computed(() => filteredProducts.value.length)
|
||||||
|
|
||||||
|
// const paginatedProducts = computed(() => {
|
||||||
|
// const start = (page.value - 1) * pageSize
|
||||||
|
// const end = start + pageSize
|
||||||
|
// return filteredProducts.value.slice(start, end)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Reset page when filters change
|
||||||
|
function resetPage() {
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to product detail
|
||||||
|
function goToProduct(product: Product) {
|
||||||
|
router.push({ name: 'product-detail', params: { id: product.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table columns
|
||||||
|
const columns = computed<TableColumn<Product>[]>(() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'image',
|
||||||
|
header: () => h('div', { class: 'text-center' }, t('products.image')),
|
||||||
|
cell: ({ row }) => h('img', {
|
||||||
|
src: row.getValue('image'),
|
||||||
|
alt: 'Product',
|
||||||
|
class: 'w-12 h-12 object-cover rounded'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('products.product_name'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const product = row.original
|
||||||
|
return h('button', {
|
||||||
|
class: 'text-primary hover:underline font-medium text-left',
|
||||||
|
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
|
||||||
|
}, product.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'code',
|
||||||
|
header: t('products.product_code'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'description',
|
||||||
|
header: t('products.description'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const desc = row.getValue('description') as string
|
||||||
|
return h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, desc?.substring(0, 50) + (desc && desc.length > 50 ? '...' : ''))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'inStock',
|
||||||
|
header: t('products.in_stock'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const inStock = row.getValue('inStock')
|
||||||
|
return h('span', {
|
||||||
|
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
|
||||||
|
}, inStock ? t('products.yes') : t('products.no'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: t('products.price'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const priceFromVal = row.original.priceFrom
|
||||||
|
const priceToVal = row.original.priceTo
|
||||||
|
return `${priceFromVal} - ${priceToVal}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'count',
|
||||||
|
header: t('products.count'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const product = row.original
|
||||||
|
return h('div', { class: 'flex gap-2' }, [
|
||||||
|
h('button', {
|
||||||
|
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
|
||||||
|
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
|
||||||
|
}, t('products.add_to_cart')),
|
||||||
|
h('button', {
|
||||||
|
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
|
||||||
|
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
|
||||||
|
}, '+')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function addToCart(product: Product) {
|
||||||
|
console.log('Add to cart:', product)
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCount(product: Product) {
|
||||||
|
product.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
searchName.value = ''
|
||||||
|
searchCode.value = ''
|
||||||
|
priceFromFilter.value = null
|
||||||
|
priceToFilter.value = null
|
||||||
|
resetPage()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container">
|
||||||
|
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
|
||||||
|
|
||||||
|
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
|
||||||
|
{{ t('products.login_to_view') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
|
||||||
|
{{ t('products.loading') }}...
|
||||||
|
</div>
|
||||||
|
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{{ productStore.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
|
||||||
|
<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]">
|
||||||
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name')
|
||||||
|
}}</label>
|
||||||
|
<UInput v-model="searchName" :placeholder="t('products.search_name_placeholder')"
|
||||||
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<UInput v-model="searchCode" :placeholder="t('products.search_code_placeholder')"
|
||||||
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<UInput v-model="priceFromFilter" type="number" :placeholder="t('products.price_from')"
|
||||||
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<UInput v-model="priceToFilter" type="number" :placeholder="t('products.price_to')"
|
||||||
|
@update:model-value="resetPage" class="dark:text-white text-black" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<button @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">
|
||||||
|
{{ t('products.clear_filters') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Products Table -->
|
||||||
|
<!-- <div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
|
||||||
|
<UTable
|
||||||
|
:data="paginatedProducts"
|
||||||
|
:columns="columns"
|
||||||
|
class="dark:text-white! text-dark"
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<!-- <div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('products.no_products') }}
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<!-- <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
||||||
|
<UPagination
|
||||||
|
v-model:page="page"
|
||||||
|
:page-count="pageSize"
|
||||||
|
:total="totalItems"
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Results count -->
|
||||||
|
<!-- <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||||
|
{{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
233
bo/src/components/customer/CartSelector.vue
Normal file
233
bo/src/components/customer/CartSelector.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
@click="toggleDropdown"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<UIcon name="mdi:cart" class="text-lg" />
|
||||||
|
<span class="text-black dark:text-white font-medium">{{ activeCartName }}</span>
|
||||||
|
<UIcon :name="isOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="absolute top-full right-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-(--border-light) dark:border-(--border-dark) rounded-lg shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<div class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
|
||||||
|
<div
|
||||||
|
v-for="cart in carts"
|
||||||
|
:key="cart.id"
|
||||||
|
class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
|
:class="{ 'bg-blue-50 dark:bg-blue-900/20': cart.id === activeCartId }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="selectCart(cart.id)"
|
||||||
|
class="flex-1 text-left"
|
||||||
|
>
|
||||||
|
<span class="text-black dark:text-white">{{ cart.name }}</span>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 text-sm ml-2">({{ cart.items.length }})</span>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
@click.stop="startEditing(cart)"
|
||||||
|
class="p-1.5 text-gray-500 hover:text-(--accent-blue-light) dark:hover:text-(--accent-blue-dark) hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
|
||||||
|
:title="t('Edit')"
|
||||||
|
>
|
||||||
|
<UIcon name="mdi:pencil" class="text-base" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="confirmDelete(cart)"
|
||||||
|
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
|
||||||
|
:title="t('Delete')"
|
||||||
|
>
|
||||||
|
<UIcon name="mdi:delete" class="text-base" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 border-t border-(--border-light) dark:border-(--border-dark)">
|
||||||
|
<button
|
||||||
|
@click="showCreateModal = true"
|
||||||
|
class="w-full flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:bg-gray-50 dark:hover:bg-gray-700 p-2 rounded"
|
||||||
|
>
|
||||||
|
<UIcon name="mdi:plus" class="text-lg" />
|
||||||
|
<span>{{ t('Create New Cart') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
@click="closeDropdown"
|
||||||
|
class="fixed inset-0 z-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showCreateModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="showCreateModal = false"
|
||||||
|
>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Create New Cart') }}</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
{{ t('Cart Name') }}
|
||||||
|
</label>
|
||||||
|
<UInput
|
||||||
|
v-model="newCartName"
|
||||||
|
:placeholder="t('Enter cart name')"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 justify-end">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
color="neutral"
|
||||||
|
@click="showCreateModal = false"
|
||||||
|
>
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
@click="createNewCart"
|
||||||
|
:disabled="!newCartName.trim()"
|
||||||
|
>
|
||||||
|
{{ t('Create and Continue') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="editingCart"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="editingCart = null"
|
||||||
|
>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Edit Cart Name') }}</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
{{ t('Cart Name') }}
|
||||||
|
</label>
|
||||||
|
<UInput
|
||||||
|
v-model="editCartName"
|
||||||
|
:placeholder="t('Enter cart name')"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 justify-end">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
color="neutral"
|
||||||
|
@click="editingCart = null"
|
||||||
|
>
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
@click="saveEditedCart"
|
||||||
|
:disabled="!editCartName.trim()"
|
||||||
|
>
|
||||||
|
{{ t('Save') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="deletingCart"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="deletingCart = null"
|
||||||
|
>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delete Cart') }}</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
{{ t('Are you sure you want to delete') }} "{{ deletingCart.name }}"?
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-3 justify-end">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
color="neutral"
|
||||||
|
@click="deletingCart = null"
|
||||||
|
>
|
||||||
|
{{ t('Cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="error"
|
||||||
|
@click="confirmDeleteCart"
|
||||||
|
>
|
||||||
|
{{ t('Delete') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useCartStore } from '@/stores/cart'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const cartStore = useCartStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
const newCartName = ref('')
|
||||||
|
const editingCart = ref<{ id: string; name: string } | null>(null)
|
||||||
|
const editCartName = ref('')
|
||||||
|
const deletingCart = ref<{ id: string; name: string } | null>(null)
|
||||||
|
|
||||||
|
const carts = computed(() => cartStore.carts)
|
||||||
|
const activeCartId = computed(() => cartStore.activeCartId)
|
||||||
|
const activeCartName = computed(() => {
|
||||||
|
const cart = cartStore.activeCart
|
||||||
|
return cart ? cart.name : t('Select Cart')
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDropdown() {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCart(cartId: string) {
|
||||||
|
cartStore.setActiveCart(cartId)
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditing(cart: { id: string; name: string }) {
|
||||||
|
editingCart.value = { id: cart.id, name: cart.name }
|
||||||
|
editCartName.value = cart.name
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditedCart() {
|
||||||
|
if (editingCart.value && editCartName.value.trim()) {
|
||||||
|
cartStore.renameCart(editingCart.value.id, editCartName.value.trim())
|
||||||
|
editingCart.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(cart: { id: string; name: string }) {
|
||||||
|
deletingCart.value = cart
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteCart() {
|
||||||
|
if (deletingCart.value) {
|
||||||
|
cartStore.deleteCart(deletingCart.value.id)
|
||||||
|
deletingCart.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewCart() {
|
||||||
|
if (newCartName.value.trim()) {
|
||||||
|
cartStore.createCart(newCartName.value.trim())
|
||||||
|
newCartName.value = ''
|
||||||
|
showCreateModal.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
198
bo/src/components/customer/PageCart.vue
Normal file
198
bo/src/components/customer/PageCart.vue
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto mt-20 flex flex-col gap-5 md:gap-10">
|
||||||
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Cart') }}</h1>
|
||||||
|
<div class="flex flex-col lg:flex-row gap-5 md:gap-10">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
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') }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="cartStore.items.length > 0">
|
||||||
|
<div v-for="item in cartStore.items" :key="item.id"
|
||||||
|
class="grid grid-cols-5 items-center p-4 border-b border-(--border-light) dark:border-(--border-dark) w-[100%]">
|
||||||
|
<div
|
||||||
|
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" />
|
||||||
|
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p class="text-black dark:text-white text-sm font-medium">{{ item.name }}</p>
|
||||||
|
<p class="text-black dark:text-white">${{ item.price.toFixed(2) }}</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" :min="1"
|
||||||
|
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
|
||||||
|
<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 v-else class="p-8 text-center">
|
||||||
|
<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>
|
||||||
|
<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') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
<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="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
|
||||||
|
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Shipping') }}</span>
|
||||||
|
<span class="text-black dark:text-white">
|
||||||
|
{{ cartStore.shippingCost > 0 ? `$${cartStore.shippingCost.toFixed(2)}` : t('Free') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<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-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-6">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
||||||
|
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light) disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{{ t('Place Order') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton block variant="outline" color="neutral" @click="cancelOrder"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-10">
|
||||||
|
<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">
|
||||||
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||||
|
class="w-full bg-white dark:bg-gray-800 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-1">
|
||||||
|
<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>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<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="cartStore.selectedDeliveryMethodId === method.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="method.id" v-model="selectedDeliveryMethod"
|
||||||
|
class="w-4 h-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark)" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-black dark:text-white font-medium">{{ method.name }}</span>
|
||||||
|
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-medium">
|
||||||
|
{{ method.price > 0 ? `$${method.price.toFixed(2)}` : t('Free') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ method.description }}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useCartStore } from '@/stores/cart'
|
||||||
|
import { useAddressStore } from '@/stores/address'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Default from '@/layouts/default.vue'
|
||||||
|
const cartStore = useCartStore()
|
||||||
|
const addressStore = useAddressStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const selectedAddress = ref<number | null>(cartStore.selectedAddressId)
|
||||||
|
const selectedDeliveryMethod = ref<number | null>(cartStore.selectedDeliveryMethodId)
|
||||||
|
const addressSearchQuery = ref('')
|
||||||
|
|
||||||
|
watch(addressSearchQuery, (val) => {
|
||||||
|
addressStore.setSearchQuery(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedAddress, (newValue) => {
|
||||||
|
cartStore.setSelectedAddress(newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedDeliveryMethod, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
cartStore.setDeliveryMethod(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const canPlaceOrder = computed(() => {
|
||||||
|
return cartStore.items.length > 0 &&
|
||||||
|
cartStore.selectedAddressId !== null &&
|
||||||
|
cartStore.selectedDeliveryMethodId !== null
|
||||||
|
})
|
||||||
|
function removeItem(itemId: number) {
|
||||||
|
cartStore.removeItem(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOrder() {
|
||||||
|
if (canPlaceOrder.value) {
|
||||||
|
router.push({ name: 'checkout' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelOrder() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
79
bo/src/components/customer/PageCheckout.vue
Normal file
79
bo/src/components/customer/PageCheckout.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="Default || 'div'">
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<div class="grid grid-cols-3 pb-6">
|
||||||
|
<button variant="outline" color="neutral" @click="goBackToCart"
|
||||||
|
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) flex items-center gap-2">
|
||||||
|
<UIcon name="mdi:arrow-left" />
|
||||||
|
{{ t('Back') }}
|
||||||
|
</button>
|
||||||
|
<h2 class="font-semibold text-black dark:text-white text-2xl text-center">
|
||||||
|
{{ t('Checkout') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden mb-6">
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
|
||||||
|
{{ t('Order Summary') }}
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Products') }}</span>
|
||||||
|
<span class="text-black dark:text-white">{{ cartStore.items.length }} {{ t('items')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
|
||||||
|
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }}</span>
|
||||||
|
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mb-6">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center gap-3 text-sm">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-white dark:bg-gray-700 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-lg text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<span class="text-black dark:text-white flex-1 truncate">{{ item.name }}</span>
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">x{{ item.quantity }}</span>
|
||||||
|
<span class="text-black dark:text-white">${{ (item.price * item.quantity).toFixed(2)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCartStore } from '@/stores/cart'
|
||||||
|
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 goBackToCart() {
|
||||||
|
router.push({ name: 'cart' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="overflow-x-auto">
|
<div v-else class="overflow-x-auto">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<CategoryMenu />
|
<CategoryMenuListing />
|
||||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||||
<template #expanded="{ row }">
|
<template #expanded="{ row }">
|
||||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||||
@@ -46,7 +46,7 @@ import { useFetchJson } from '@/composable/useFetchJson'
|
|||||||
import Default from '@/layouts/default.vue'
|
import Default from '@/layouts/default.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { TableColumn } from '@nuxt/ui'
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
import CategoryMenu from '../inner/categoryMenu.vue'
|
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
reference: number
|
reference: number
|
||||||
|
|||||||
@@ -1,57 +1,20 @@
|
|||||||
<template>
|
|
||||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getMenu } from '@/router/menu'
|
import { getMenu } from '@/router/menu'
|
||||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
let menu = await getMenu() as NavigationMenuItem[]
|
let menu = await getMenu() as NavigationMenuItem[]
|
||||||
|
|
||||||
const openAll = ref(false)
|
const openAll = ref(false)
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
let categoryId = ref(route.params.category_id)
|
|
||||||
function findPath(tree: NavigationMenuItem[], id: number, path: Array<number> = []): Array<number> | null {
|
|
||||||
for (let item of tree) {
|
|
||||||
let newPath: Array<number> = [...path, item.category_id]
|
|
||||||
if (item.category_id === id) {
|
|
||||||
return newPath
|
|
||||||
}
|
|
||||||
if (item.children) {
|
|
||||||
const result: Array<number> | null = findPath(item.children, id, newPath)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = findPath(menu, Number(categoryId.value))
|
|
||||||
function adaptMenu(menu: NavigationMenuItem[]) {
|
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||||
for (const item of menu) {
|
for (const item of menu) {
|
||||||
if (item.children && item.children.length > 0) {
|
if (item.children && item.children.length > 0) {
|
||||||
item.open = path && path.includes(item.category_id) ? true : openAll.value
|
console.log(item);
|
||||||
adaptMenu(item.children);
|
adaptMenu(item.children);
|
||||||
item.children.unshift({
|
item.open = openAll.value
|
||||||
label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: {
|
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
|
||||||
name: 'customer-products-category', params: {
|
|
||||||
category_id: item.params.category_id,
|
|
||||||
link_rewrite: item.params.link_rewrite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
item.to = {
|
item.to = { name: 'category', params: item.params };
|
||||||
name: 'customer-products-category', params: {
|
|
||||||
category_id: item.params.category_id,
|
|
||||||
link_rewrite: item.params.link_rewrite
|
|
||||||
}
|
|
||||||
};
|
|
||||||
item.icon = 'i-lucide-file-text'
|
item.icon = 'i-lucide-file-text'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +22,7 @@ function adaptMenu(menu: NavigationMenuItem[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu = adaptMenu(menu)
|
menu = adaptMenu(menu)
|
||||||
|
|
||||||
const items = ref<NavigationMenuItem[][]>([
|
const items = ref<NavigationMenuItem[][]>([
|
||||||
[
|
[
|
||||||
...menu as NavigationMenuItem[]
|
...menu as NavigationMenuItem[]
|
||||||
@@ -66,3 +30,7 @@ const items = ref<NavigationMenuItem[][]>([
|
|||||||
|
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
|
||||||
|
</template>
|
||||||
|
|||||||
35
bo/src/components/inner/categoryMenuListing.vue
Normal file
35
bo/src/components/inner/categoryMenuListing.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getMenu } from '@/router/menu'
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
let menu = await getMenu() as NavigationMenuItem[]
|
||||||
|
|
||||||
|
const openAll = ref(false)
|
||||||
|
|
||||||
|
function adaptMenu(menu: NavigationMenuItem[]) {
|
||||||
|
for (const item of menu) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
adaptMenu(item.children);
|
||||||
|
item.open = openAll.value
|
||||||
|
item.children.unshift({ label: item.label, icon: 'i-lucide-book-open', popover: item.label, to: { name: 'category', params: item.params } })
|
||||||
|
} else {
|
||||||
|
item.to = { name: 'category', params: item.params };
|
||||||
|
item.icon = 'i-lucide-file-text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = adaptMenu(menu)
|
||||||
|
|
||||||
|
const items = ref<NavigationMenuItem[][]>([
|
||||||
|
[
|
||||||
|
...menu as NavigationMenuItem[]
|
||||||
|
],
|
||||||
|
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UNavigationMenu orientation="vertical" type="single" :items="items" class="p-4" />
|
||||||
|
</template>
|
||||||
@@ -59,6 +59,7 @@ async function setRoutes() {
|
|||||||
const componentName = item.component
|
const componentName = item.component
|
||||||
const [, folder] = componentName.split('/')
|
const [, folder] = componentName.split('/')
|
||||||
const componentPath = `/src${componentName}`
|
const componentPath = `/src${componentName}`
|
||||||
|
console.log(componentPath);
|
||||||
|
|
||||||
|
|
||||||
let modules =
|
let modules =
|
||||||
@@ -113,3 +114,4 @@ router.beforeEach((to, from) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { useFetchJson } from "@/composable/useFetchJson";
|
import { useFetchJson } from "@/composable/useFetchJson";
|
||||||
import type { MenuItem, Route } from "@/types/menu";
|
import type { MenuItem, Route } from "@/types/menu";
|
||||||
import { ref } from "vue";
|
|
||||||
import { settings } from "./settings";
|
|
||||||
|
|
||||||
|
|
||||||
const categoryId = ref()
|
|
||||||
export const getMenu = async () => {
|
export const getMenu = async () => {
|
||||||
if(!categoryId.value){
|
const resp = await useFetchJson<MenuItem>('/api/v1/restricted/menu/get-category-tree');
|
||||||
categoryId.value = settings['app'].category_tree_root_id
|
|
||||||
}
|
|
||||||
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${categoryId.value}`);
|
|
||||||
return resp.items.children
|
return resp.items.children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,16 @@ export interface DeliveryMethod {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Cart {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
items: CartItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export const useCartStore = defineStore('cart', () => {
|
export const useCartStore = defineStore('cart', () => {
|
||||||
const items = ref<CartItem[]>([])
|
const carts = ref<Cart[]>([])
|
||||||
|
const activeCartId = ref<string | null>(null)
|
||||||
|
|
||||||
const selectedAddressId = ref<number | null>(null)
|
const selectedAddressId = ref<number | null>(null)
|
||||||
const selectedDeliveryMethodId = ref<number | null>(null)
|
const selectedDeliveryMethodId = ref<number | null>(null)
|
||||||
const shippingCost = ref(0)
|
const shippingCost = ref(0)
|
||||||
@@ -31,13 +39,15 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
{ id: 3, name: 'Priority Delivery', price: 30, description: 'Next business day' }
|
{ id: 3, name: 'Priority Delivery', price: 30, description: 'Next business day' }
|
||||||
])
|
])
|
||||||
|
|
||||||
function initMockData() {
|
const items = computed(() => {
|
||||||
items.value = [
|
if (!activeCartId.value) return []
|
||||||
{ id: 1, productId: 101, name: 'Premium Widget Pro', product_number: 'NC209/7000', image: '/img/product-1.jpg', price: 129.99, quantity: 2 },
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
{ id: 2, productId: 102, name: 'Ultra Gadget X', product_number: 'NC234/6453', image: '/img/product-2.jpg', price: 89.50, quantity: 1 },
|
return cart ? cart.items : []
|
||||||
{ id: 3, productId: 103, name: 'Mega Tool Set', product_number: 'NC324/9030', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
|
})
|
||||||
]
|
|
||||||
}
|
const activeCart = computed(() => {
|
||||||
|
return carts.value.find(c => c.id === activeCartId.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
const productsTotal = computed(() => {
|
const productsTotal = computed(() => {
|
||||||
return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||||
@@ -55,8 +65,63 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
return items.value.reduce((sum, item) => sum + item.quantity, 0)
|
return items.value.reduce((sum, item) => sum + item.quantity, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function createCart(name: string): Cart {
|
||||||
|
const newCart: Cart = {
|
||||||
|
id: `cart-${Date.now()}`,
|
||||||
|
name,
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
carts.value.push(newCart)
|
||||||
|
activeCartId.value = newCart.id
|
||||||
|
|
||||||
|
return newCart
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCart(cartId: string) {
|
||||||
|
const index = carts.value.findIndex(c => c.id === cartId)
|
||||||
|
if (index !== -1) {
|
||||||
|
carts.value.splice(index, 1)
|
||||||
|
if (activeCartId.value === cartId) {
|
||||||
|
const firstCart = carts.value[0]
|
||||||
|
activeCartId.value = firstCart ? firstCart.id : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameCart(cartId: string, newName: string) {
|
||||||
|
const cart = carts.value.find(c => c.id === cartId)
|
||||||
|
if (cart) {
|
||||||
|
cart.name = newName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveCart(cartId: string) {
|
||||||
|
const cart = carts.value.find(c => c.id === cartId)
|
||||||
|
if (cart) {
|
||||||
|
activeCartId.value = cartId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItemToActiveCart(item: CartItem) {
|
||||||
|
if (!activeCartId.value) {
|
||||||
|
createCart('Cart 1')
|
||||||
|
}
|
||||||
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
|
if (cart) {
|
||||||
|
const existingItem = cart.items.find(i => i.productId === item.productId)
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity += item.quantity
|
||||||
|
} else {
|
||||||
|
cart.items.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateQuantity(itemId: number, quantity: number) {
|
function updateQuantity(itemId: number, quantity: number) {
|
||||||
const item = items.value.find(i => i.id === itemId)
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
|
if (!cart) return
|
||||||
|
|
||||||
|
const item = cart.items.find(i => i.id === itemId)
|
||||||
if (item) {
|
if (item) {
|
||||||
if (quantity <= 0) {
|
if (quantity <= 0) {
|
||||||
removeItem(itemId)
|
removeItem(itemId)
|
||||||
@@ -67,12 +132,14 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteProduct(id: number): boolean {
|
function deleteProduct(id: number): boolean {
|
||||||
const index = items.value.findIndex(a => a.id === id)
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
|
if (!cart) return false
|
||||||
|
|
||||||
|
const index = cart.items.findIndex(a => a.id === id)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
items.value.splice(index, 1)
|
cart.items.splice(index, 1)
|
||||||
resetProductPagination()
|
resetProductPagination()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +148,20 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeItem(itemId: number) {
|
function removeItem(itemId: number) {
|
||||||
const index = items.value.findIndex(i => i.id === itemId)
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
|
if (!cart) return
|
||||||
|
|
||||||
|
const index = cart.items.findIndex(i => i.id === itemId)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
items.value.splice(index, 1)
|
cart.items.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCart() {
|
function clearCart() {
|
||||||
items.value = []
|
const cart = carts.value.find(c => c.id === activeCartId.value)
|
||||||
|
if (cart) {
|
||||||
|
cart.items = []
|
||||||
|
}
|
||||||
selectedAddressId.value = null
|
selectedAddressId.value = null
|
||||||
selectedDeliveryMethodId.value = null
|
selectedDeliveryMethodId.value = null
|
||||||
shippingCost.value = 0
|
shippingCost.value = 0
|
||||||
@@ -106,9 +179,40 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initMockData() {
|
||||||
|
const cart1: Cart = {
|
||||||
|
id: 'cart-1',
|
||||||
|
name: 'Cart 1',
|
||||||
|
items: [
|
||||||
|
{ 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', product_number: 'NC234/6453', image: '/img/product-2.jpg', price: 89.50, quantity: 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart2: Cart = {
|
||||||
|
id: 'cart-2',
|
||||||
|
name: 'Cart 2',
|
||||||
|
items: [
|
||||||
|
{ id: 3, productId: 103, name: 'Mega Tool Set', product_number: 'NC324/9030', image: '/img/product-3.jpg', price: 249.00, quantity: 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart3: Cart = {
|
||||||
|
id: 'cart-3',
|
||||||
|
name: 'Cart 3',
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
|
||||||
|
carts.value = [cart1, cart2, cart3]
|
||||||
|
activeCartId.value = 'cart-1'
|
||||||
|
}
|
||||||
|
|
||||||
initMockData()
|
initMockData()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
carts,
|
||||||
|
activeCartId,
|
||||||
|
activeCart,
|
||||||
items,
|
items,
|
||||||
selectedAddressId,
|
selectedAddressId,
|
||||||
selectedDeliveryMethodId,
|
selectedDeliveryMethodId,
|
||||||
@@ -119,8 +223,14 @@ export const useCartStore = defineStore('cart', () => {
|
|||||||
vatAmount,
|
vatAmount,
|
||||||
orderTotal,
|
orderTotal,
|
||||||
itemCount,
|
itemCount,
|
||||||
deleteProduct,
|
|
||||||
|
createCart,
|
||||||
|
deleteCart,
|
||||||
|
renameCart,
|
||||||
|
setActiveCart,
|
||||||
|
addItemToActiveCart,
|
||||||
updateQuantity,
|
updateQuantity,
|
||||||
|
deleteProduct,
|
||||||
removeItem,
|
removeItem,
|
||||||
clearCart,
|
clearCart,
|
||||||
setSelectedAddress,
|
setSelectedAddress,
|
||||||
|
|||||||
@@ -13,24 +13,24 @@ const products = [
|
|||||||
// type CategoryProducts = {}
|
// type CategoryProducts = {}
|
||||||
|
|
||||||
export const useCategoryStore = defineStore('category', () => {
|
export const useCategoryStore = defineStore('category', () => {
|
||||||
const idCategory = ref(0)
|
const id_category = ref(0)
|
||||||
const categoryProducts = ref(products)
|
const categoryProducts = ref(products)
|
||||||
|
|
||||||
|
|
||||||
function setCategoryID(id: number) {
|
function setCategoryID(id: number) {
|
||||||
idCategory.value = id
|
id_category.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCategoryProducts() {
|
async function getCategoryProducts() {
|
||||||
return new Promise<typeof products>((resolve) => {
|
return new Promise<typeof products>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// console.log('Fetching products from category id: ', idCategory.value);
|
console.log('Fetching products from category id: ', id_category.value);
|
||||||
resolve(categoryProducts.value)
|
resolve(categoryProducts.value)
|
||||||
}, 2000 * Math.random())
|
}, 2000 * Math.random())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
idCategory,
|
id_category,
|
||||||
getCategoryProducts,
|
getCategoryProducts,
|
||||||
setCategoryID
|
setCategoryID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
|
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
|
||||||
)
|
)
|
||||||
productDescription.value = response.items
|
productDescription.value = response.items
|
||||||
|
console.log(productDescription, 'dfsfsdf');
|
||||||
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load product description'
|
error.value = e instanceof Error ? e.message : 'Failed to load product description'
|
||||||
@@ -44,21 +45,31 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function stripHtml(html: string) {
|
||||||
async function saveProductDescription(productID?: number) {
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = html
|
||||||
|
return div.textContent || div.innerText || ''
|
||||||
|
}
|
||||||
|
async function saveProductDescription(productID?: number, langId?: number) {
|
||||||
const id = productID || 1
|
const id = productID || 1
|
||||||
|
const lang = langId || 1
|
||||||
try {
|
try {
|
||||||
const data = await useFetchJson(
|
const data = await useFetchJson(
|
||||||
`/api/v1/restricted/product-description/save-product-description?productID=${id}&productShopID=1&productLangID=1`,
|
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(
|
headers: {
|
||||||
{
|
'Content-Type': 'application/json'
|
||||||
description: productDescription.value.description,
|
},
|
||||||
description_short: productDescription.value.description_short,
|
body: JSON.stringify({
|
||||||
meta_description: productDescription.value.meta_description,
|
name: stripHtml(productDescription.value?.name || ''),
|
||||||
available_now: productDescription.value.available_now,
|
description: stripHtml(productDescription.value?.description || ''),
|
||||||
usage: productDescription.value.usage
|
description_short: stripHtml(productDescription.value?.description_short || ''),
|
||||||
|
meta_title: stripHtml(productDescription.value?.meta_title || ''),
|
||||||
|
meta_description: stripHtml(productDescription.value?.meta_description || ''),
|
||||||
|
available_now: stripHtml(productDescription.value?.available_now || ''),
|
||||||
|
available_later: stripHtml(productDescription.value?.available_later || ''),
|
||||||
|
usage: stripHtml(productDescription.value?.usage || '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -68,12 +79,12 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function translateProductDescription(productID: number, fromLangId: number, toLangId: number) {
|
async function translateProductDescription(productID: number, fromLangId: number, toLangId: number, model: string = 'OpenAI') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-description/translate-product-description?productID=${productID}&productShopID=1&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=OpenAI`)
|
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=${model}`)
|
||||||
productDescription.value = response.items
|
productDescription.value = response.items
|
||||||
return response.items
|
return response.items
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
10
bo/src/types/product.d.ts
vendored
10
bo/src/types/product.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
export interface ProductDescription {
|
export interface ProductDescription {
|
||||||
id?: number
|
id?: number
|
||||||
name?: string
|
name?: string
|
||||||
description: string
|
description: string
|
||||||
@@ -7,11 +7,3 @@ export interface ProductDescription {
|
|||||||
available_now: string
|
available_now: string
|
||||||
usage: string
|
usage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Product {
|
|
||||||
reference: number
|
|
||||||
product_id: number
|
|
||||||
name: string
|
|
||||||
image_link: string
|
|
||||||
link_rewrite: string
|
|
||||||
}
|
|
||||||
1
bo/src/types/settings.d.ts
vendored
1
bo/src/types/settings.d.ts
vendored
@@ -7,7 +7,6 @@ export interface Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
category_tree_root_id: number
|
|
||||||
name: string
|
name: string
|
||||||
environment: string
|
environment: string
|
||||||
base_url: string
|
base_url: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: add-new-cart
|
name: add-new-cart
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 11
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: add-product-to-cart (1)
|
name: add-product-to-cart (1)
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 16
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: add-product-to-cart
|
name: add-product-to-cart
|
||||||
type: http
|
type: http
|
||||||
seq: 14
|
seq: 15
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: change-cart-name
|
name: change-cart-name
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 12
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
info:
|
info:
|
||||||
name: create-index
|
name: create-index
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 7
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: http://localhost:3000/api/v1/restricted/search/create-index
|
url: http://localhost:3000/api/v1/restricted/meili-search/create-index
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: get-breadcrumb
|
name: get-breadcrumb
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 18
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: get-category-tree
|
name: get-category-tree
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 5
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: get-indexes
|
name: get-indexes
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 9
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
info:
|
info:
|
||||||
name: get-product-description
|
name: get-product-description
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 17
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=1
|
url: http://localhost:3000/api/v1/restricted/product-translation/get-product-description?productID=51&productLangID=4
|
||||||
params:
|
params:
|
||||||
- name: productID
|
- name: productID
|
||||||
value: "51"
|
value: "51"
|
||||||
type: query
|
type: query
|
||||||
- name: productLangID
|
- name: productLangID
|
||||||
value: "1"
|
value: "4"
|
||||||
type: query
|
type: query
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: get_countries
|
name: get_countries
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 4
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: list-users
|
name: list-users
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 2
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: remove-index
|
name: remove-index
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 8
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: retrieve-cart
|
name: retrieve-cart
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 14
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: retrieve-carts-info
|
name: retrieve-carts-info
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 13
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
info:
|
|
||||||
name: save-product-description
|
|
||||||
type: http
|
|
||||||
seq: 19
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=3
|
|
||||||
params:
|
|
||||||
- name: productID
|
|
||||||
value: "1"
|
|
||||||
type: query
|
|
||||||
- name: productLangID
|
|
||||||
value: "3"
|
|
||||||
type: query
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: |-
|
|
||||||
{
|
|
||||||
"description": "<p>Der Einsatz von Rehabilitationsrollen in verschiedenen Übungen und Behandlungen wirkt sich positiv auf die Reduzierung von Verletzungen und die Genesungschancen aus. Sie werden in der Rehabilitation, bei Korrekturgymnastik sowie in der traditionellen und Sportmassage eingesetzt, da sie ideal zum Anheben und Spreizen von Gliedmaßen geeignet sind. Zudem können sie zur Unterstützung von Knien, Füßen, Armen und Schultern verwendet werden. Auch für Kinder sind Rehabilitationsrollen empfehlenswert; ihre spielerische Anwendung fördert die Entwicklung der Grobmotorik.</p><p> Dank der großen Auswahl an Farben und Größen lässt sich ein Übungsset zusammenstellen, das in jeder Physiotherapiepraxis, jedem Massageraum, jeder Schule oder jedem Kindergarten benötigt wird.</p><p> Die Rehabilitationsrolle ist ein Medizinprodukt, das den grundlegenden Anforderungen an Medizinprodukte und den Bestimmungen des Medizinproduktegesetzes entspricht, im Register für Medizinprodukte des Amtes für die Registrierung von Arzneimitteln, Medizinprodukten und Biozidprodukten eingetragen ist, mit der Konformitätserklärung des Herstellers versehen ist und das CE-Zeichen trägt. </p><p></p><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/images.jpg\" alt=\"Medizinprodukt\" style=\"margin-left:auto;margin-right:auto;\" width=\"253\" height=\"86\" /></p><h4> <strong>Empfohlene Verwendung:</strong></h4><ul style=\"list-style-type:circle;\"><li> in der Rehabilitation</li><li> während Massagen (traditionell, Sport)</li><li> in der Korrekturgymnastik (insbesondere für Kinder)</li><li> zur Linderung von Verletzungen einzelner Körperteile</li><li> Zur Unterstützung von: Knien, Knöcheln, Kopf des Patienten</li><li> bei Übungen zur Entwicklung der motorischen Fähigkeiten von Kindern</li><li> in Schönheitssalons</li><li> in Kinderspielzimmern</li></ul><p></p><h4> <strong>Materialspezifikationen:</strong></h4><p> <strong>Abdeckung:</strong> PVC-beschichtetes Material, das für medizinische Geräte vorgesehen ist und daher sehr leicht zu reinigen und zu desinfizieren ist:</p><ul style=\"list-style-type:circle;\"><li> Material gemäß REACH-Verordnung, zertifiziert mit dem STANDARD 100 Zertifikat von OEKO-TEX®.</li><li> Enthält keine Phthalate</li><li> feuerfest</li><li> resistent gegenüber physiologischen Flüssigkeiten (Blut, Urin, Schweiß) und Alkohol</li><li> UV-beständig, daher auch für den Einsatz im Freien geeignet.</li><li> kratzfest</li><li> ölbeständig </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/reach.jpg\" alt=\"ERREICHEN\" width=\"115\" height=\"115\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"116\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/phthalate-free.jpg\" alt=\"Enthält keine Phthalate\" width=\"112\" height=\"111\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/fireresistant.jpg\" alt=\"Feuerfest\" width=\"114\" height=\"113\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-alkohol.jpg\" alt=\"Alkoholbeständig\" width=\"114\" height=\"114\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-uv.jpg\" alt=\"UV-beständig\" width=\"117\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/outdoor.jpg\" alt=\"Für den Einsatz im Freien konzipiert\" width=\"116\" height=\"116\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/odporny-na-zadrapania.jpg\" alt=\"Kratzfest\" width=\"97\" height=\"96\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/olejoodporny.jpg\" alt=\"Ölbeständig\" width=\"99\" height=\"98\" /></p><p> <strong>Füllung:</strong> mittelharter Polyurethanschaum mit erhöhter Verformungsbeständigkeit:</p><ul style=\"list-style-type:circle;\"><li> besitzt ein Hygienezertifikat, ausgestellt vom Institut für Maritime und Tropenmedizin in Gdynia</li><li> zertifiziert mit dem STANDARD 100 by OEKO-TEX® Zertifikat – Produktklasse I, ausgestellt vom Textilforschungsinstitut in Łódź</li><li> Hergestellt aus hochwertigen Rohstoffen, die die Ozonschicht nicht schädigen. </li></ul><p><img src=\"https://www.naluconcept.com/img/cms/Logotypy/oeko-tex.jpg\" alt=\"Öko-Tex Standard 100 Zertifikat\" width=\"95\" height=\"95\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/Logo_GUMed_kolor-180x180.jpg\" alt=\"Hygienezertifikat\" width=\"94\" height=\"94\" /><img src=\"https://www.naluconcept.com/img/cms/Logotypy/atest_higieniczny_kolor.jpg\" alt=\"Hygienezertifikat\" width=\"79\" height=\"94\" /></p><p></p><p></p>"
|
|
||||||
}
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
info:
|
info:
|
||||||
name: search
|
name: search
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 10
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: http://localhost:3000/api/v1/restricted/search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0
|
url: http://localhost:3000/api/v1/restricted/meili-search/search?query=w&limit=4&id_category=0&price_lower_bound=60.0&price_upper_bound=70.0
|
||||||
params:
|
params:
|
||||||
- name: query
|
- name: query
|
||||||
value: w
|
value: w
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
info:
|
info:
|
||||||
name: test
|
name: test
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 6
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: http://localhost:3000/api/v1/restricted/search/test
|
url: http://localhost:3000/api/v1/restricted/meili-search/test
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
info:
|
|
||||||
name: translate-product-description
|
|
||||||
type: http
|
|
||||||
seq: 21
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=1&productToLangID=3&model=Google
|
|
||||||
params:
|
|
||||||
- name: productID
|
|
||||||
value: "51"
|
|
||||||
type: query
|
|
||||||
- name: productFromLangID
|
|
||||||
value: "1"
|
|
||||||
type: query
|
|
||||||
- name: productToLangID
|
|
||||||
value: "3"
|
|
||||||
type: query
|
|
||||||
- name: model
|
|
||||||
value: Google
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: update-choice
|
name: update-choice
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 3
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: auth
|
|
||||||
type: folder
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: carts
|
|
||||||
type: folder
|
|
||||||
seq: 7
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: langs-and-countries
|
|
||||||
type: folder
|
|
||||||
seq: 4
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: list
|
|
||||||
type: folder
|
|
||||||
seq: 3
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: menu
|
|
||||||
type: folder
|
|
||||||
seq: 5
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: product-translation
|
|
||||||
type: folder
|
|
||||||
seq: 2
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,28 +0,0 @@
|
|||||||
info:
|
|
||||||
name: translate-product-description
|
|
||||||
type: http
|
|
||||||
seq: 24
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=2&productToLangID=3&model=Google
|
|
||||||
params:
|
|
||||||
- name: productID
|
|
||||||
value: "51"
|
|
||||||
type: query
|
|
||||||
- name: productFromLangID
|
|
||||||
value: "2"
|
|
||||||
type: query
|
|
||||||
- name: productToLangID
|
|
||||||
value: "3"
|
|
||||||
type: query
|
|
||||||
- name: model
|
|
||||||
value: Google
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: search
|
|
||||||
type: folder
|
|
||||||
seq: 6
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
info:
|
|
||||||
name: copy
|
|
||||||
type: http
|
|
||||||
seq: 7
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/copy/folder1/test.txt?dest_path=/folder/a.txt
|
|
||||||
params:
|
|
||||||
- name: dest_path
|
|
||||||
value: /folder/a.txt
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
info:
|
|
||||||
name: create-folder
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/create-folder?name=folder
|
|
||||||
params:
|
|
||||||
- name: name
|
|
||||||
value: folder
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: delete-file
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/delete-file/folder1/TODO.txt
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: delete-folder
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/delete-folder/folder/
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: download-file
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/download-file/folder1/test.xlsx
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
info:
|
|
||||||
name: storage
|
|
||||||
type: folder
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
request:
|
|
||||||
auth: inherit
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: list-content
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/list-content/folder1
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
info:
|
|
||||||
name: move
|
|
||||||
type: http
|
|
||||||
seq: 8
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/move/folder?dest_path=/folder1/test.txt
|
|
||||||
params:
|
|
||||||
- name: dest_path
|
|
||||||
value: /folder1/test.txt
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
info:
|
|
||||||
name: upload-file
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: http://localhost:3000/api/v1/restricted/storage/upload-file/folder1/
|
|
||||||
body:
|
|
||||||
type: multipart-form
|
|
||||||
data:
|
|
||||||
- name: document
|
|
||||||
type: file
|
|
||||||
value:
|
|
||||||
- /home/daniel/TODO.txt
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This is a test.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This is a test.
|
|
||||||
Reference in New Issue
Block a user