Compare commits
5 Commits
front-styl
...
currencies
| Author | SHA1 | Date | |
|---|---|---|---|
| 25afec6e1c | |||
| f1363b153d | |||
| decf2e9f8a | |||
| 93a7dd1718 | |||
| ce3d82f101 |
@@ -28,7 +28,7 @@ tmp_dir = "tmp"
|
|||||||
rerun = false
|
rerun = false
|
||||||
rerun_delay = 500
|
rerun_delay = 500
|
||||||
send_interrupt = false
|
send_interrupt = false
|
||||||
stop_on_error = false
|
stop_on_error = true
|
||||||
|
|
||||||
[color]
|
[color]
|
||||||
app = ""
|
app = ""
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
|
||||||
@@ -33,8 +35,9 @@ func NewCurrencyHandler() *CurrencyHandler {
|
|||||||
func CurrencyHandlerRoutes(r fiber.Router) fiber.Router {
|
func CurrencyHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
handler := NewCurrencyHandler()
|
handler := NewCurrencyHandler()
|
||||||
|
|
||||||
r.Post("/currency-rate", middleware.Require(perms.CurrencyWrite), handler.PostCurrencyRate)
|
r.Patch("", middleware.Require(perms.CurrencyWrite), handler.PostCurrencyRate)
|
||||||
r.Get("/currency-rate/:id", handler.GetCurrencyRate)
|
r.Get("/list", handler.List)
|
||||||
|
r.Get("/:id", handler.GetCurrencyRate)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +53,8 @@ func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
|
|||||||
|
|
||||||
logger.Error("failed to create currency rate",
|
logger.Error("failed to create currency rate",
|
||||||
"handler", "CurrencyHandler.PostCurrencyRate",
|
"handler", "CurrencyHandler.PostCurrencyRate",
|
||||||
|
"b2b_id_currency", currencyRate.B2bIdCurrency,
|
||||||
|
"conversion_rate", currencyRate.ConversionRate,
|
||||||
"error", err.Error(),
|
"error", err.Error(),
|
||||||
)
|
)
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
@@ -65,16 +69,13 @@ func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
|
|||||||
id, err := strconv.Atoi(idStr)
|
id, err := strconv.Atoi(idStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currency, err := h.CurrencyService.GetCurrency(uint(id))
|
currency, err := h.CurrencyService.Get(uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
logger.Error("failed to get currency",
|
logger.Error("failed to get currency",
|
||||||
"handler", "CurrencyHandler.GetCurrencyRate",
|
"handler", "CurrencyHandler.GetCurrencyRate",
|
||||||
|
"b2b_id_currency", id,
|
||||||
"currency_id", id,
|
|
||||||
"error", err.Error(),
|
"error", err.Error(),
|
||||||
)
|
)
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
@@ -82,3 +83,37 @@ func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
|
|||||||
|
|
||||||
return c.JSON(response.Make(currency, 0, i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(currency, 0, i18n.T_(c, response.Message_OK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *CurrencyHandler) List(c fiber.Ctx) error {
|
||||||
|
langId, ok := localeExtractor.GetLangID(c)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
p, filt, err := query_params.ParseFilters[model.Currency](c, columnMappingCurrencies)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := h.CurrencyService.Find(langId, p, filt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to get currency list",
|
||||||
|
"handler", "CurrencyHandler.List",
|
||||||
|
"lang_id", langId,
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnMappingCurrencies map[string]string = map[string]string{
|
||||||
|
"id": "c.id",
|
||||||
|
"ps_id_currency": "c.ps_id_currency",
|
||||||
|
"is_default": "c.is_default",
|
||||||
|
"is_active": "c.is_active",
|
||||||
|
"conversion_rate": "r.conversion_rate",
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ func (s *Server) Setup() error {
|
|||||||
restricted.StorageHandlerRoutes(restrictedStorage)
|
restricted.StorageHandlerRoutes(restrictedStorage)
|
||||||
webdav.StorageHandlerRoutes(webdavStorage)
|
webdav.StorageHandlerRoutes(webdavStorage)
|
||||||
|
|
||||||
restricted.CurrencyHandlerRoutes(s.restricted)
|
restrictedCurrency := s.restricted.Group("/currency-rate")
|
||||||
|
restricted.CurrencyHandlerRoutes(restrictedCurrency)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type Currency struct {
|
|||||||
PsIDCurrency uint `json:"ps_id_currency"`
|
PsIDCurrency uint `json:"ps_id_currency"`
|
||||||
IsDefault bool `json:"is_default"`
|
IsDefault bool `json:"is_default"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
ConversionRate *float64 `json:"conversion_rate,omitempty"`
|
ConversionRate *float64 `json:"conversion_rate,omitempty" gorm:"column:conversion_rate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Currency) TableName() string {
|
func (Currency) TableName() string {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UICurrencyRepo interface {
|
type UICurrencyRepo interface {
|
||||||
CreateConversionRate(currencyRate *model.CurrencyRate) error
|
CreateConversionRate(currencyRate *model.CurrencyRate) error
|
||||||
Get(id uint) (*model.Currency, error)
|
Get(id uint) (*model.Currency, error)
|
||||||
|
Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrencyRepo struct{}
|
type CurrencyRepo struct{}
|
||||||
@@ -25,19 +27,12 @@ func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate)
|
|||||||
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
|
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
|
||||||
var currency model.Currency
|
var currency model.Currency
|
||||||
|
|
||||||
err := db.DB.Table("b2b_currencies c").
|
err := db.DB.
|
||||||
Select("c.*, r.conversion_rate").
|
Model(&model.Currency{}).
|
||||||
Joins(`
|
Scopes(WithLatestRate()).
|
||||||
LEFT JOIN b2b_currency_rates r
|
Select("b2b_currencies.*, r.conversion_rate").
|
||||||
ON r.b2b_id_currency = c.id
|
Where("b2b_currencies.id = ?", id).
|
||||||
AND r.created_at = (
|
First(¤cy).Error
|
||||||
SELECT MAX(created_at)
|
|
||||||
FROM b2b_currency_rates
|
|
||||||
WHERE b2b_id_currency = c.id
|
|
||||||
)
|
|
||||||
`).
|
|
||||||
Where("c.id = ?", id).
|
|
||||||
Scan(¤cy).Error
|
|
||||||
|
|
||||||
return ¤cy, err
|
return ¤cy, err
|
||||||
}
|
}
|
||||||
@@ -46,8 +41,24 @@ func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.Filters
|
|||||||
|
|
||||||
found, err := find.Paginate[model.Currency](langId, p, db.DB.
|
found, err := find.Paginate[model.Currency](langId, p, db.DB.
|
||||||
Model(&model.Currency{}).
|
Model(&model.Currency{}).
|
||||||
|
Scopes(WithLatestRate()).
|
||||||
|
Select("b2b_currencies.*, r.conversion_rate").
|
||||||
Scopes(filt.All()...),
|
Scopes(filt.All()...),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &found, err
|
return &found, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithLatestRate() func(db *gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Joins(`
|
||||||
|
LEFT JOIN b2b_currency_rates r
|
||||||
|
ON r.b2b_id_currency = b2b_currencies.id
|
||||||
|
AND r.created_at = (
|
||||||
|
SELECT MAX(created_at)
|
||||||
|
FROM b2b_currency_rates
|
||||||
|
WHERE b2b_id_currency = b2b_currencies.id
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,16 +3,22 @@ package currencyService
|
|||||||
import (
|
import (
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo"
|
"git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CurrencyService struct {
|
type CurrencyService struct {
|
||||||
repo currencyRepo.UICurrencyRepo
|
repo currencyRepo.UICurrencyRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CurrencyService) GetCurrency(id uint) (*model.Currency, error) {
|
func (s *CurrencyService) Get(id uint) (*model.Currency, error) {
|
||||||
return s.repo.Get(id)
|
return s.repo.Get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *CurrencyService) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
|
||||||
|
return s.repo.Find(langId, p, filt)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error {
|
func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error {
|
||||||
return s.repo.CreateConversionRate(currency)
|
return s.repo.CreateConversionRate(currency)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error
|
|||||||
var items []T
|
var items []T
|
||||||
var count int64
|
var count int64
|
||||||
|
|
||||||
stmt.Count(&count)
|
countStmt := stmt.Session(&gorm.Session{}).Select("count(*)").Offset(-1).Limit(-1)
|
||||||
|
|
||||||
|
if err := countStmt.Count(&count).Error; err != nil {
|
||||||
|
return Found[T]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
err := stmt.
|
err := stmt.
|
||||||
Offset(paging.Offset()).
|
Offset(paging.Offset()).
|
||||||
@@ -42,15 +46,10 @@ func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error
|
|||||||
return Found[T]{}, err
|
return Found[T]{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// columnsSpec := GetColumnsSpec[T](langID)
|
|
||||||
|
|
||||||
return Found[T]{
|
return Found[T]{
|
||||||
Items: items,
|
Items: items,
|
||||||
Count: uint(count),
|
Count: uint(count),
|
||||||
// Spec: map[string]interface{}{
|
}, nil
|
||||||
// "columns": columnsSpec,
|
|
||||||
// },
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetColumnsSpec[T any] generates a column specification map for a given struct type T.
|
// GetColumnsSpec[T any] generates a column specification map for a given struct type T.
|
||||||
|
|||||||
4
bo/components.d.ts
vendored
4
bo/components.d.ts
vendored
@@ -11,7 +11,6 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
'>': typeof import('./src/components/admin/product/ <TabGeneralSkeleton v-if="addProductStore.loadingCategories" />.vue')['default']
|
|
||||||
AddProduct: typeof import('./src/components/admin/AddProduct.vue')['default']
|
AddProduct: typeof import('./src/components/admin/AddProduct.vue')['default']
|
||||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||||
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
|
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
|
||||||
@@ -23,7 +22,6 @@ declare module 'vue' {
|
|||||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||||
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
|
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
|
||||||
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
|
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
|
||||||
LayoutSkeleton: typeof import('./src/components/ui/LayoutSkeleton.vue')['default']
|
|
||||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||||
@@ -49,8 +47,6 @@ declare module 'vue' {
|
|||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
|
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
|
||||||
TabGeneral: typeof import('./src/components/admin/product/TabGeneral.vue')['default']
|
TabGeneral: typeof import('./src/components/admin/product/TabGeneral.vue')['default']
|
||||||
TabGeneralSceleton: typeof import('./src/components/admin/product/TabGeneralSceleton.vue')['default']
|
|
||||||
TabGeneralSkeleton: typeof import('./src/components/admin/product/TabGeneralSkeleton.vue')['default']
|
|
||||||
TabOptions: typeof import('./src/components/admin/product/TabOptions.vue')['default']
|
TabOptions: typeof import('./src/components/admin/product/TabOptions.vue')['default']
|
||||||
TabPricing: typeof import('./src/components/admin/product/TabPricing.vue')['default']
|
TabPricing: typeof import('./src/components/admin/product/TabPricing.vue')['default']
|
||||||
TabQuantities: typeof import('./src/components/admin/product/TabQuantities.vue')['default']
|
TabQuantities: typeof import('./src/components/admin/product/TabQuantities.vue')['default']
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<TabGeneralSkeleton v-if="addProductStore.loadingCategories" />
|
<div class="space-y-5">
|
||||||
|
{{ addProductStore.form }}
|
||||||
<div v-else class="space-y-5">
|
|
||||||
<!-- {{ addProductStore.form }} -->
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<UFormField label="Product name" name="productName">
|
<UFormField label="Product name" name="productName">
|
||||||
@@ -25,6 +23,7 @@
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3 bg-white dark:bg-neutral-900">
|
class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3 bg-white dark:bg-neutral-900">
|
||||||
<legend class="px-1 block font-medium text-default text-[16px]">Images</legend>
|
<legend class="px-1 block font-medium text-default text-[16px]">Images</legend>
|
||||||
|
{{ addProductStore.images }}
|
||||||
|
|
||||||
<div class="flex items-start gap-3 flex-wrap">
|
<div class="flex items-start gap-3 flex-wrap">
|
||||||
<div v-for="(img, idx) in addProductStore.images" :key="idx" class="relative group shrink-0">
|
<div v-for="(img, idx) in addProductStore.images" :key="idx" class="relative group shrink-0">
|
||||||
@@ -79,25 +78,27 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<legend class="px-1 block font-medium text-default text-[16px]">Powiązany produkt</legend>
|
<p class="text-sm font-medium text-black dark:text-white">Powiązany produkt</p>
|
||||||
|
|
||||||
<UInput v-model="relatedSearch" placeholder="Search and add Powiązany produkt" icon="i-lucide-search"
|
<UInput v-model="relatedSearch" placeholder="Search and add Powiązany produkt" icon="i-lucide-search"
|
||||||
class="w-full" />
|
class="w-full" />
|
||||||
|
|
||||||
<div v-if="addProductStore.relatedProducts.length > 0"
|
<div v-if="addProductStore.relatedProducts.length > 0"
|
||||||
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||||
<div v-for="p in addProductStore.relatedProducts" :key="p.product_id"
|
<div v-for="p in addProductStore.relatedProducts" :key="p.id_product"
|
||||||
class="flex items-center justify-between px-3 py-2 text-sm text-black dark:text-white border-b border-(--border-light) dark:border-(--border-dark) last:border-0 bg-white">
|
class="flex items-center justify-between px-3 py-2 text-sm text-black dark:text-white border-b border-(--border-light) dark:border-(--border-dark) last:border-0">
|
||||||
<span>{{ p.name }}</span>
|
<span>{{ p.name }}</span>
|
||||||
<button type="button" @click="removeRelated(p.product_id)"
|
<button type="button" @click="removeRelated(p.id_product)"
|
||||||
class="text-red-500 transition-colors ml-2 cursor-pointer hover:text-red-700">
|
class="text-gray-400 hover:text-red-500 transition-colors ml-2">
|
||||||
<UIcon name="i-lucide-x" class="text-base" />
|
<UIcon name="i-lucide-x" class="text-base" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<USelect :items="relatedResults" value-key="product_id" />
|
||||||
<div v-if="relatedResults.length > 0"
|
<div v-if="relatedResults.length > 0"
|
||||||
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) bg-white dark:bg-neutral-900 max-h-80 overflow-auto">
|
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden bg-white dark:bg-neutral-900">
|
||||||
<button v-for="p in relatedResults" :key="p.product_id" type="button"
|
<button v-for="p in relatedResults" :key="p.id_product" type="button"
|
||||||
class="w-full text-left px-3 py-2 text-sm text-black dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors border-b border-(--border-light) dark:border-(--border-dark) last:border-0"
|
class="w-full text-left px-3 py-2 text-sm text-black dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors border-b border-(--border-light) dark:border-(--border-dark) last:border-0"
|
||||||
@click="addRelated(p)">
|
@click="addRelated(p)">
|
||||||
{{ p.name }}
|
{{ p.name }}
|
||||||
@@ -126,12 +127,14 @@
|
|||||||
|
|
||||||
<div class="space-y-0.5 overflow-y-auto flex">
|
<div class="space-y-0.5 overflow-y-auto flex">
|
||||||
<UNavigationMenu orientation="vertical" type="multiple" :items="filteredCategories" :ui="{
|
<UNavigationMenu orientation="vertical" type="multiple" :items="filteredCategories" :ui="{
|
||||||
root: 'w-auto max-h-80'
|
root: 'w-auto'
|
||||||
}">
|
}">
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div class="flex items-center gap-2.5 cursor-pointer rounded px-1.5 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors"
|
<div
|
||||||
|
class="flex items-center gap-2.5 cursor-pointer rounded px-1.5 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors"
|
||||||
@click.stop="toggleCategory({ id_category: item.params?.category_id, name: item.label as string })">
|
@click.stop="toggleCategory({ id_category: item.params?.category_id, name: item.label as string })">
|
||||||
<UCheckbox :model-value="isCategorySelected(item.params?.category_id)" color="info" />
|
<UCheckbox :model-value="isCategorySelected(item.params?.category_id)"
|
||||||
|
color="info" />
|
||||||
<span class="text-sm text-black dark:text-white">{{ item.label }}</span>
|
<span class="text-sm text-black dark:text-white">{{ item.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -153,7 +156,6 @@ import errorImg from '@/assets/error.svg'
|
|||||||
import { useFetchJson } from '@/composable/useFetchJson'
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
import { watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
import TabGeneralSkeleton from './TabGeneralSkeleton.vue'
|
|
||||||
|
|
||||||
const addProductStore = useAddProductStore()
|
const addProductStore = useAddProductStore()
|
||||||
|
|
||||||
@@ -183,6 +185,12 @@ function adaptCategory(menu: NavigationMenuItem[]) {
|
|||||||
if (item.children && item.children.length > 0) {
|
if (item.children && item.children.length > 0) {
|
||||||
item.open = true
|
item.open = true
|
||||||
adaptCategory(item.children);
|
adaptCategory(item.children);
|
||||||
|
|
||||||
|
item.children.unshift({
|
||||||
|
label: item.label,
|
||||||
|
icon: 'i-lucide-book-open',
|
||||||
|
params: item.params,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
item.icon = 'i-lucide-file-text'
|
item.icon = 'i-lucide-file-text'
|
||||||
}
|
}
|
||||||
@@ -190,7 +198,7 @@ function adaptCategory(menu: NavigationMenuItem[]) {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
//filter category - return like parent => children
|
//there have to be request on filter category (but have to return like parent => children =>)
|
||||||
const filteredCategories = computed(() => {
|
const filteredCategories = computed(() => {
|
||||||
const q = categorySearch.value.trim().toLowerCase()
|
const q = categorySearch.value.trim().toLowerCase()
|
||||||
if (!q) return allCategories.value
|
if (!q) return allCategories.value
|
||||||
@@ -202,6 +210,8 @@ function isCategorySelected(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleCategory(cat: ProductCategory) {
|
function toggleCategory(cat: ProductCategory) {
|
||||||
|
console.log(cat);
|
||||||
|
|
||||||
if (isCategorySelected(cat.id_category)) {
|
if (isCategorySelected(cat.id_category)) {
|
||||||
removeCategory(cat.id_category)
|
removeCategory(cat.id_category)
|
||||||
} else {
|
} else {
|
||||||
@@ -228,7 +238,7 @@ watch(relatedSearch, (q) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function addRelated(product: ProductRelated) {
|
function addRelated(product: ProductRelated) {
|
||||||
if (!addProductStore.relatedProducts.some(p => p.product_id === product.product_id)) {
|
if (!addProductStore.relatedProducts.some(p => p.id_product === product.id_product)) {
|
||||||
addProductStore.relatedProducts.push(product)
|
addProductStore.relatedProducts.push(product)
|
||||||
}
|
}
|
||||||
relatedSearch.value = ''
|
relatedSearch.value = ''
|
||||||
@@ -236,7 +246,7 @@ function addRelated(product: ProductRelated) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeRelated(id: number) {
|
function removeRelated(id: number) {
|
||||||
const idx = addProductStore.relatedProducts.findIndex(p => p.product_id === id)
|
const idx = addProductStore.relatedProducts.findIndex(p => p.id_product === id)
|
||||||
if (idx !== -1) addProductStore.relatedProducts.splice(idx, 1)
|
if (idx !== -1) addProductStore.relatedProducts.splice(idx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,5 +274,5 @@ async function fetchProducts(q: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addProductStore.loadCategories()
|
await addProductStore.loadCategories()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="space-y-5 animate-pulse">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<div class="h-3.5 w-24 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<div class="h-3.5 w-28 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
|
|
||||||
<div class="h-4 w-16 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<div v-for="i in 3" :key="i" class="w-36 h-36 rounded-xl bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="h-3.5 w-32 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
|
|
||||||
<div class="h-4 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-9 w-52 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div v-for="i in 5" :key="i" class="flex items-center gap-2.5 px-1.5 py-1">
|
|
||||||
<div class="h-4 w-4 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
|
||||||
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700"
|
|
||||||
:class="['w-32', 'w-48', 'w-40', 'w-36', 'w-44'][i - 1]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
|
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
|
||||||
:ui="{ base: 'p-8', root: 'p-2' }"
|
:ui="{ base: 'p-8 sm:px-16', root: 'p-2' }"
|
||||||
class="min-w-full border rounded-md bg-white! border-(--border-light)" placeholder="Write there ...">
|
class="min-w-full border rounded-md bg-white! border-(--border-light)" placeholder="Write there ...">
|
||||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8 flex-wrap!">
|
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8 flex-wrap!">
|
||||||
<template #link>
|
<template #link>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-1 overflow-x-hidden h-svh animate-pulse">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="flex flex-col shrink-0 w-52 bg-elevated/25 border-r border-default h-full">
|
|
||||||
<!-- Sidebar header -->
|
|
||||||
<div class="flex items-center gap-2 p-3 border-b border-default h-(--ui-header-height)">
|
|
||||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
|
||||||
<div class="h-4 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar nav items -->
|
|
||||||
<div class="flex flex-col gap-1 p-2 flex-1">
|
|
||||||
<div v-for="i in 7" :key="i" class="flex items-center gap-2.5 px-1.5 py-1.5">
|
|
||||||
<div class="h-5 w-5 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
|
||||||
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700" :class="['w-20','w-28','w-24','w-16','w-28','w-20','w-24'][i-1]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar footer -->
|
|
||||||
<div class="p-3 border-t border-default">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
|
||||||
<div class="h-3.5 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main area -->
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
|
|
||||||
<!-- Left: toggle + title -->
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-5 w-36 rounded bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
<!-- Right: controls -->
|
|
||||||
<div class="hidden md:flex items-center gap-3">
|
|
||||||
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
<div class="h-8 w-24 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Page content placeholder -->
|
|
||||||
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutSkeleton v-if="loadingLayout" />
|
<div class="flex flex-1 overflow-x-hidden h-svh">
|
||||||
|
|
||||||
<div v-else class="flex flex-1 overflow-x-hidden h-svh">
|
|
||||||
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
||||||
container: 'h-full z-80',
|
container: 'h-full z-80',
|
||||||
inner: 'bg-elevated/25 divide-transparent',
|
inner: 'bg-elevated/25 divide-transparent',
|
||||||
@@ -90,7 +88,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import LayoutSkeleton from '@/components/ui/LayoutSkeleton.vue'
|
|
||||||
import { useColorMode } from '@vueuse/core'
|
import { useColorMode } from '@vueuse/core'
|
||||||
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
|
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
|
||||||
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
|
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
|
||||||
@@ -101,9 +98,7 @@ const userStore = useUserStore()
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||||
|
await userStore.getUser()
|
||||||
const loadingLayout = ref(true)
|
|
||||||
userStore.getUser().finally(() => { loadingLayout.value = false })
|
|
||||||
|
|
||||||
const open = ref(true)
|
const open = ref(true)
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const menu = ref<TopMenuItem[] | null>(null)
|
|||||||
const Id = Number(route.params.user_id)
|
const Id = Number(route.params.user_id)
|
||||||
async function cmGetTopMenu() {
|
async function cmGetTopMenu() {
|
||||||
try {
|
try {
|
||||||
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-customer-management-menu`)
|
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
|
||||||
|
|
||||||
menu.value = items
|
menu.value = items
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -136,12 +136,9 @@ export const useAddProductStore = defineStore('addProduct', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categories = ref<MenuItem[]>([])
|
const categories = ref<MenuItem[]>([])
|
||||||
const loadingCategories = ref(true)
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
loadingCategories.value = true
|
|
||||||
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${settings['app'].category_tree_root_id}`);
|
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${settings['app'].category_tree_root_id}`);
|
||||||
categories.value = resp.items.children
|
categories.value = resp.items.children
|
||||||
loadingCategories.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -157,7 +154,6 @@ export const useAddProductStore = defineStore('addProduct', () => {
|
|||||||
variantSaving,
|
variantSaving,
|
||||||
variantErrors,
|
variantErrors,
|
||||||
categories,
|
categories,
|
||||||
loadingCategories,
|
|
||||||
loadProduct,
|
loadProduct,
|
||||||
addImageFiles,
|
addImageFiles,
|
||||||
removeImage,
|
removeImage,
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const data = await useFetchJson<User>(`/api/v1/restricted/customer`)
|
const data = await useFetchJson<User>(`/api/v1/restricted/customer`)
|
||||||
|
console.log('getUser API response:', data)
|
||||||
|
|
||||||
const response: User = (data as any).items ?? data
|
const response: User = (data as any).items ?? data
|
||||||
|
console.log('User response:', response)
|
||||||
user.value = response
|
user.value = response
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err?.message ?? 'Unknown error'
|
error.value = err?.message ?? 'Unknown error'
|
||||||
|
|||||||
2
bo/src/types/product.d.ts
vendored
2
bo/src/types/product.d.ts
vendored
@@ -25,7 +25,7 @@ export interface ProductCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductRelated {
|
export interface ProductRelated {
|
||||||
product_id: number
|
id_product: number
|
||||||
name: string
|
name: string
|
||||||
reference: string
|
reference: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
info:
|
info:
|
||||||
name: currency
|
name: Currency
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 1
|
||||||
|
|
||||||
15
bruno/api_v1/currency/List.yml
Normal file
15
bruno/api_v1/currency/List.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: List
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{bas_url}}/restricted/currency-rate/list"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
info:
|
info:
|
||||||
name: currency-rate
|
name: Update currency rate
|
||||||
type: http
|
type: http
|
||||||
seq: 2
|
seq: 2
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: PATCH
|
||||||
url: "{{bas_url}}/restricted/currency-rate"
|
url: "{{bas_url}}/restricted/currency-rate"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
@@ -43,6 +43,9 @@ INSERT INTO `b2b_currencies` (`ps_id_currency`, `is_default`, `is_active`) VALUE
|
|||||||
('1','1','1'),
|
('1','1','1'),
|
||||||
('2','0','1');
|
('2','0','1');
|
||||||
|
|
||||||
|
INSERT INTO `b2b_currency_rates` (`id`, `b2b_id_currency`, `created_at`, `conversion_rate`) VALUES ('1', '1', '2026-04-17 14:32:03', '1.000000');
|
||||||
|
INSERT INTO `b2b_currency_rates` (`id`, `b2b_id_currency`, `created_at`, `conversion_rate`) VALUES ('2', '2', '2026-04-17 14:32:03', '4.600000');
|
||||||
|
|
||||||
INSERT IGNORE INTO b2b_countries
|
INSERT IGNORE INTO b2b_countries
|
||||||
(id, flag, ps_id_country, b2b_id_currency)
|
(id, flag, ps_id_country, b2b_id_currency)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
Reference in New Issue
Block a user