Compare commits
1 Commits
mailisearc
...
99fe11fbeb
| Author | SHA1 | Date | |
|---|---|---|---|
| 99fe11fbeb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,3 @@ bin/
|
|||||||
i18n/*.json
|
i18n/*.json
|
||||||
*_templ.go
|
*_templ.go
|
||||||
tmp/main
|
tmp/main
|
||||||
test.go
|
|
||||||
12
Taskfile.yml
12
Taskfile.yml
@@ -73,22 +73,10 @@ vars:
|
|||||||
MP_SMTP_AUTH_ALLOW_INSECURE: true
|
MP_SMTP_AUTH_ALLOW_INSECURE: true
|
||||||
MP_ENABLE_SPAMASSASSIN: postmark
|
MP_ENABLE_SPAMASSASSIN: postmark
|
||||||
MP_VERBOSE: true
|
MP_VERBOSE: true
|
||||||
meilisearch:
|
|
||||||
image: getmeili/meilisearch:latest
|
|
||||||
container_name: meilisearch
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- 7700:7700
|
|
||||||
volumes:
|
|
||||||
- meilisearch:/data.ms
|
|
||||||
environment:
|
|
||||||
MEILI_MASTER_KEY: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
mailpit_data:
|
mailpit_data:
|
||||||
meilisearch:
|
|
||||||
|
|
||||||
|
|
||||||
includes:
|
includes:
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package restricted
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/menuService"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/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 MenuHandler struct {
|
|
||||||
menuService *menuService.MenuService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMenuHandler() *MenuHandler {
|
|
||||||
menuService := menuService.New()
|
|
||||||
return &MenuHandler{
|
|
||||||
menuService: menuService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func MenuHandlerRoutes(r fiber.Router) fiber.Router {
|
|
||||||
handler := NewMenuHandler()
|
|
||||||
|
|
||||||
r.Get("/get-menu", handler.GetMenu)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MenuHandler) GetMenu(c fiber.Ctx) error {
|
|
||||||
|
|
||||||
id_shop_attribute := c.Query("shopID")
|
|
||||||
id_shop, err := strconv.Atoi(id_shop_attribute)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
id_lang, err := strconv.Atoi(c.Cookies("lang_id", "2"))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
|
||||||
}
|
|
||||||
|
|
||||||
menu, err := h.menuService.GetMenu(uint(id_shop), uint(id_lang))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(response.Make(&menu, 0, i18n.T_(c, response.Message_OK)))
|
|
||||||
}
|
|
||||||
@@ -102,10 +102,6 @@ func (s *Server) Setup() error {
|
|||||||
langsAndCountries := s.restricted.Group("/langs-and-countries")
|
langsAndCountries := s.restricted.Group("/langs-and-countries")
|
||||||
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries)
|
restricted.LangsAndCountriesHandlerRoutes(langsAndCountries)
|
||||||
|
|
||||||
// menu (restricted)
|
|
||||||
menu := s.restricted.Group("/menu")
|
|
||||||
restricted.MenuHandlerRoutes(menu)
|
|
||||||
|
|
||||||
// // Restricted routes example
|
// // Restricted routes example
|
||||||
// restricted := s.api.Group("/restricted")
|
// restricted := s.api.Group("/restricted")
|
||||||
// restricted.Use(middleware.AuthMiddleware())
|
// restricted.Use(middleware.AuthMiddleware())
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ type Product struct {
|
|||||||
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
||||||
}
|
}
|
||||||
type ProductInList struct {
|
type ProductInList struct {
|
||||||
ProductID uint `gorm:"column:ID;primaryKey" json:"product_id" form:"product_id"`
|
ID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
||||||
Name string `gorm:"column:name" json:"name" form:"name"`
|
Name string `gorm:"column:name;default:'no name'" json:"name" form:"name"`
|
||||||
ImageID uint `gorm:"column:id_image"`
|
Price float64 `gorm:"column:price;default:0.0" json:"price" form:"price"`
|
||||||
LinkRewrite string `gorm:"column:link_rewrite"`
|
ActiveAsProduct uint `gorm:"column:active;default:0" json:"active_as_product" form:"active_as_product"`
|
||||||
Active uint `gorm:"column:active" json:"active" form:"active"`
|
ActiveInShop uint `gorm:"column:active;default:0" json:"active_in_shop" form:"active_in_shop"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductFilters struct {
|
type ProductFilters struct {
|
||||||
@@ -81,19 +81,4 @@ type ProductFilters struct {
|
|||||||
InStock uint `query:"stock,omitempty"`
|
InStock uint `query:"stock,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScannedCategory struct {
|
|
||||||
CategoryID uint `gorm:"column:ID;primaryKey"`
|
|
||||||
Name string `gorm:"column:name"`
|
|
||||||
Active uint `gorm:"column:active"`
|
|
||||||
Position uint `gorm:"column:position"`
|
|
||||||
ParentID uint `gorm:"column:id_parent"`
|
|
||||||
IsRoot uint `gorm:"column:is_root_category"`
|
|
||||||
}
|
|
||||||
type Category struct {
|
|
||||||
CategoryID uint `json:"category_id" form:"category_id"`
|
|
||||||
Name string `json:"name" form:"name"`
|
|
||||||
Active uint `json:"active" form:"active"`
|
|
||||||
Subcategories []Category `json:"subcategories" form:"subcategories"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FeatVal = map[uint][]uint
|
type FeatVal = map[uint][]uint
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package menuService
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/repository/categoriesRepo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MenuService struct {
|
|
||||||
categoriesRepo categoriesRepo.UICategoriesRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *MenuService {
|
|
||||||
return &MenuService{
|
|
||||||
categoriesRepo: categoriesRepo.New(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MenuService) GetMenu(id_shop uint, id_lang uint) (model.Category, error) {
|
|
||||||
all_categories, err := s.categoriesRepo.GetAllCategories(id_shop, id_lang)
|
|
||||||
if err != nil {
|
|
||||||
return model.Category{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the root
|
|
||||||
root_index := 0
|
|
||||||
root_found := false
|
|
||||||
for i := 0; i < len(all_categories); i++ {
|
|
||||||
if all_categories[i].IsRoot == 1 {
|
|
||||||
root_index = i
|
|
||||||
root_found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !root_found {
|
|
||||||
return model.Category{}, responseErrors.ErrNoRootFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// now create the children and reorder them according to position
|
|
||||||
id_to_index := make(map[uint]int)
|
|
||||||
for i := 0; i < len(all_categories); i++ {
|
|
||||||
id_to_index[all_categories[i].CategoryID] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
children_indices := make(map[int][]ChildWithPosition)
|
|
||||||
for i := 0; i < len(all_categories); i++ {
|
|
||||||
parent_index := id_to_index[all_categories[i].ParentID]
|
|
||||||
children_indices[parent_index] = append(children_indices[parent_index], ChildWithPosition{Index: i, Position: all_categories[i].Position})
|
|
||||||
}
|
|
||||||
|
|
||||||
for key := range children_indices {
|
|
||||||
sort.Sort(ByPosition(children_indices[key]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally, create the tree
|
|
||||||
tree := s.createTree(root_index, &all_categories, &children_indices)
|
|
||||||
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MenuService) createTree(index int, all_categories *([]model.ScannedCategory), children_indices *(map[int][]ChildWithPosition)) model.Category {
|
|
||||||
node := s.scannedToNormalCategory((*all_categories)[index])
|
|
||||||
|
|
||||||
for i := 0; i < len((*children_indices)[index]); i++ {
|
|
||||||
node.Subcategories = append(node.Subcategories, s.createTree((*children_indices)[index][i].Index, all_categories, children_indices))
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) model.Category {
|
|
||||||
var normal model.Category
|
|
||||||
normal.Active = scanned.Active
|
|
||||||
normal.CategoryID = scanned.CategoryID
|
|
||||||
normal.Name = scanned.Name
|
|
||||||
normal.Subcategories = []model.Category{}
|
|
||||||
return normal
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChildWithPosition struct {
|
|
||||||
Index int
|
|
||||||
Position uint
|
|
||||||
}
|
|
||||||
type ByPosition []ChildWithPosition
|
|
||||||
|
|
||||||
func (a ByPosition) Len() int { return len(a) }
|
|
||||||
func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position }
|
|
||||||
@@ -48,9 +48,6 @@ var (
|
|||||||
|
|
||||||
// Typed errors for product list handler
|
// Typed errors for product list handler
|
||||||
ErrBadPaging = errors.New("bad or missing paging attribute value in header")
|
ErrBadPaging = errors.New("bad or missing paging attribute value in header")
|
||||||
|
|
||||||
// Typed errors for menu handler
|
|
||||||
ErrNoRootFound = errors.New("no root found in categories table")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents an error with HTTP status code
|
// Error represents an error with HTTP status code
|
||||||
@@ -138,9 +135,6 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrBadPaging):
|
case errors.Is(err, ErrBadPaging):
|
||||||
return i18n.T_(c, "error.err_bad_paging")
|
return i18n.T_(c, "error.err_bad_paging")
|
||||||
|
|
||||||
case errors.Is(err, ErrNoRootFound):
|
|
||||||
return i18n.T_(c, "error.no_root_found")
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return i18n.T_(c, "error.err_internal_server_error")
|
return i18n.T_(c, "error.err_internal_server_error")
|
||||||
}
|
}
|
||||||
@@ -175,8 +169,7 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrBadAttribute),
|
errors.Is(err, ErrBadAttribute),
|
||||||
errors.Is(err, ErrBadField),
|
errors.Is(err, ErrBadField),
|
||||||
errors.Is(err, ErrInvalidXHTML),
|
errors.Is(err, ErrInvalidXHTML),
|
||||||
errors.Is(err, ErrBadPaging),
|
errors.Is(err, ErrBadPaging):
|
||||||
errors.Is(err, ErrNoRootFound):
|
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
|
|||||||
1
bo/components.d.ts
vendored
1
bo/components.d.ts
vendored
@@ -19,6 +19,7 @@ declare module 'vue' {
|
|||||||
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']
|
||||||
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||||
|
PageProductsList: typeof import('./src/components/customer/PageProductsList.vue')['default']
|
||||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ const authStore = useAuthStore()
|
|||||||
<RouterLink :to="{ name: 'cart' }">
|
<RouterLink :to="{ name: 'cart' }">
|
||||||
Cart
|
Cart
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'products-list' }">
|
||||||
|
Products List
|
||||||
|
</RouterLink>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const authStore = useAuthStore()
|
|||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
|
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
|
||||||
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
|
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
|
||||||
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
<span class="font-semibold text-gray-900 dark:text-white">B2B</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<!-- Right Side Actions -->
|
<!-- Right Side Actions -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto mt-10">
|
<div class="container mx-auto mt-10">
|
||||||
<div class="flex flex-col mb-6">
|
<div class="flex flex-col gap-5 mb-6">
|
||||||
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
|
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
|
||||||
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
|
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div v-if="paginatedAddresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div v-for="address in paginatedAddresses" :key="address.id"
|
<div v-for="address in paginatedAddresses" :key="address.id"
|
||||||
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow">
|
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow">
|
||||||
@@ -35,11 +34,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
|
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-center">
|
<div class="mt-6 flex justify-center">
|
||||||
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
|
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
|
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-6 flex flex-col gap-6">
|
<div class="p-6 flex flex-col gap-6">
|
||||||
@@ -74,7 +71,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|
||||||
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
|
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-6 flex flex-col gap-3">
|
<div class="p-6 flex flex-col gap-3">
|
||||||
@@ -106,7 +102,6 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const addressStore = useAddressStore()
|
const addressStore = useAddressStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
@@ -122,17 +117,14 @@ const totalItems = computed(() => addressStore.totalItems)
|
|||||||
const pageSize = addressStore.pageSize
|
const pageSize = addressStore.pageSize
|
||||||
|
|
||||||
watch(page, (newPage) => addressStore.setPage(newPage))
|
watch(page, (newPage) => addressStore.setPage(newPage))
|
||||||
|
|
||||||
watch(searchQuery, (val) => {
|
watch(searchQuery, (val) => {
|
||||||
addressStore.setSearchQuery(val)
|
addressStore.setSearchQuery(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
resetForm()
|
resetForm()
|
||||||
isEditing.value = false
|
isEditing.value = false
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(address: any) {
|
function openEditModal(address: any) {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
street: address.street,
|
street: address.street,
|
||||||
@@ -144,17 +136,14 @@ function openEditModal(address: any) {
|
|||||||
editingAddressId.value = address.id
|
editingAddressId.value = address.id
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
formData.value = { street: '', zipCode: '', city: '', country: '' }
|
formData.value = { street: '', zipCode: '', city: '', country: '' }
|
||||||
editingAddressId.value = null
|
editingAddressId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
const errors = []
|
const errors = []
|
||||||
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
|
if (!formData.value.street) errors.push({ name: 'street', message: 'Street required' })
|
||||||
@@ -163,7 +152,6 @@ function validate() {
|
|||||||
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
|
if (!formData.value.country) errors.push({ name: 'country', message: 'Country required' })
|
||||||
return errors.length ? errors : null
|
return errors.length ? errors : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAddress() {
|
function saveAddress() {
|
||||||
if (validate()) return
|
if (validate()) return
|
||||||
if (isEditing.value && editingAddressId.value) {
|
if (isEditing.value && editingAddressId.value) {
|
||||||
@@ -173,12 +161,18 @@ function saveAddress() {
|
|||||||
}
|
}
|
||||||
closeModal()
|
closeModal()
|
||||||
}
|
}
|
||||||
|
// const Lera = ref('')
|
||||||
|
// function run (){
|
||||||
|
// if(Lera.value==='lera'){
|
||||||
|
// console.log('Leraa okokok')
|
||||||
|
// }else{
|
||||||
|
// console.log('LEra nonono')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
function confirmDelete(id: number) {
|
function confirmDelete(id: number) {
|
||||||
addressToDelete.value = id
|
addressToDelete.value = id
|
||||||
showDeleteConfirm.value = true
|
showDeleteConfirm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAddress() {
|
function deleteAddress() {
|
||||||
if (addressToDelete.value) {
|
if (addressToDelete.value) {
|
||||||
addressStore.deleteAddress(addressToDelete.value)
|
addressStore.deleteAddress(addressToDelete.value)
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
|
<h1 class="text-2xl font-bold text-black dark:text-white mb-8">{{ t('Shopping Cart') }}</h1>
|
||||||
<div class="flex flex-col lg:flex-row gap-8 mb-8">
|
<div class="flex flex-col lg:flex-row gap-8 mb-8">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
<div
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||||
|
<h2
|
||||||
|
class="text-lg font-semibold text-black dark:text-white p-4 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
{{ t('Selected Products') }}
|
{{ t('Selected Products') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
<div
|
||||||
|
class="hidden md:grid grid-cols-12 gap-4 p-4 bg-(--second-light) dark:bg-(--main-dark) text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
<div class="col-span-4">{{ t('Product') }}</div>
|
<div class="col-span-4">{{ t('Product') }}</div>
|
||||||
<div class="col-span-2 text-right">{{ t('Price') }}</div>
|
<div class="col-span-2 text-right">{{ t('Price') }}</div>
|
||||||
<div class="col-span-3 text-center">{{ t('Quantity') }}</div>
|
<div class="col-span-3 text-center">{{ t('Quantity') }}</div>
|
||||||
@@ -18,7 +21,8 @@
|
|||||||
<div v-for="item in cartStore.items" :key="item.id"
|
<div v-for="item in cartStore.items" :key="item.id"
|
||||||
class="grid grid-cols-1 md:grid-cols-12 gap-4 p-4 border-b border-(--border-light) dark:border-(--border-dark) items-center">
|
class="grid grid-cols-1 md:grid-cols-12 gap-4 p-4 border-b border-(--border-light) dark:border-(--border-dark) items-center">
|
||||||
<div class="col-span-4 flex items-center gap-4">
|
<div class="col-span-4 flex items-center gap-4">
|
||||||
<div class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
<div
|
||||||
|
class="w-16 h-16 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
|
||||||
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
|
||||||
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
<UIcon v-else name="mdi:package-variant" class="text-2xl text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -29,22 +33,18 @@
|
|||||||
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
|
<span class="text-black dark:text-white">${{ item.price.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-3 flex items-center justify-center">
|
<div class="col-span-3 flex items-center justify-center">
|
||||||
<div class="flex items-center border border-(--border-light) dark:border-(--border-dark) rounded">
|
<UInputNumber v-model="item.quantity" :min="1"
|
||||||
<button @click="decreaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
|
||||||
<UIcon name="mdi:minus" />
|
|
||||||
</button>
|
|
||||||
<span class="px-3 py-1 text-black dark:text-white min-w-[40px] text-center">{{ item.quantity }}</span>
|
|
||||||
<button @click="increaseQuantity(item)" class="px-3 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
||||||
<UIcon name="mdi:plus" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 text-right">
|
<div class="col-span-2 text-right">
|
||||||
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Total') }}: </span>
|
<span class="md:hidden text-gray-500 dark:text-gray-400 text-sm">{{ t('Total') }}: </span>
|
||||||
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</span>
|
<span class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-1 flex justify-center">
|
<div class="col-span-1 flex justify-center">
|
||||||
<button @click="removeItem(item.id)" class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors" :title="t('Remove')">
|
<button @click="removeItem(item.id)"
|
||||||
|
class="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
||||||
|
:title="t('Remove')">
|
||||||
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
<UIcon name="material-symbols:delete" class="text-[20px]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,14 +53,16 @@
|
|||||||
<div v-else class="p-8 text-center">
|
<div v-else class="p-8 text-center">
|
||||||
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
<UIcon name="mdi:cart-outline" class="text-6xl text-gray-300 dark:text-gray-600 mb-4" />
|
||||||
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
|
||||||
<RouterLink :to="{ name: 'product-card-full' }" class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
<RouterLink :to="{ name: 'product-card-full' }"
|
||||||
|
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
{{ t('Continue Shopping') }}
|
{{ t('Continue Shopping') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-80">
|
<div class="lg:w-80">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6 sticky top-24">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Order Summary') }}</h2>
|
||||||
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
@@ -74,13 +76,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0) }}%)</span>
|
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }} ({{ (cartStore.vatRate * 100).toFixed(0)
|
||||||
|
}}%)</span>
|
||||||
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mb-6">
|
<div class="flex justify-between mb-6">
|
||||||
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
|
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
|
||||||
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{ cartStore.orderTotal.toFixed(2) }}</span>
|
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">${{
|
||||||
|
cartStore.orderTotal.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
<UButton block color="primary" @click="placeOrder" :disabled="!canPlaceOrder"
|
||||||
@@ -97,7 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col lg:flex-row gap-8">
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Select Delivery Address') }}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
<UInput v-model="addressSearchQuery" type="text" :placeholder="t('Search address')"
|
||||||
@@ -105,8 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
<div v-if="addressStore.filteredAddresses.length > 0" class="space-y-3">
|
||||||
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
<label v-for="address in addressStore.filteredAddresses" :key="address.id"
|
||||||
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
class="flex items-start gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedAddressId === address.id
|
||||||
:class="cartStore.selectedAddressId === address.id
|
|
||||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
<input type="radio" :value="address.id" v-model="selectedAddress"
|
<input type="radio" :value="address.id" v-model="selectedAddress"
|
||||||
@@ -121,19 +125,20 @@
|
|||||||
<div v-else class="text-center py-6">
|
<div v-else class="text-center py-6">
|
||||||
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
<UIcon name="mdi:map-marker-outline" class="text-4xl text-gray-400 mb-2" />
|
||||||
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
<p class="text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</p>
|
||||||
<RouterLink :to="{ name: 'addresses' }" class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
<RouterLink :to="{ name: 'addresses' }"
|
||||||
|
class="inline-block mt-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
|
||||||
{{ t('Add Address') }}
|
{{ t('Add Address') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
<div
|
||||||
|
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) p-6">
|
||||||
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
<h2 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delivery Method') }}</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
<label v-for="method in cartStore.deliveryMethods" :key="method.id"
|
||||||
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors"
|
class="flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors" :class="cartStore.selectedDeliveryMethodId === method.id
|
||||||
:class="cartStore.selectedDeliveryMethodId === method.id
|
|
||||||
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
? 'border-(--accent-blue-light) dark:border-(--accent-blue-dark) bg-blue-50 dark:bg-blue-900/20'
|
||||||
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
: 'border-(--border-light) dark:border-(--border-dark) hover:border-gray-400'">
|
||||||
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
|
<input type="radio" :value="method.id" v-model="selectedDeliveryMethod"
|
||||||
@@ -157,7 +162,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useCartStore, type CartItem } from '@/stores/cart'
|
import { useCartStore } from '@/stores/cart'
|
||||||
import { useAddressStore } from '@/stores/address'
|
import { useAddressStore } from '@/stores/address'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -190,15 +195,6 @@ const canPlaceOrder = computed(() => {
|
|||||||
cartStore.selectedAddressId !== null &&
|
cartStore.selectedAddressId !== null &&
|
||||||
cartStore.selectedDeliveryMethodId !== null
|
cartStore.selectedDeliveryMethodId !== null
|
||||||
})
|
})
|
||||||
|
|
||||||
function increaseQuantity(item: CartItem) {
|
|
||||||
cartStore.updateQuantity(item.id, item.quantity + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function decreaseQuantity(item: CartItem) {
|
|
||||||
cartStore.updateQuantity(item.id, item.quantity - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(itemId: number) {
|
function removeItem(itemId: number) {
|
||||||
cartStore.removeItem(itemId)
|
cartStore.removeItem(itemId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mt-14 mx-auto">
|
<div class="container mt-14 mx-auto">
|
||||||
<div class="flex justify-between gap-8 mb-6">
|
<div class="flex md:flex-row flex-col justify-between gap-8 mb-6">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
||||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-end mb-8">
|
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -49,10 +49,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<ProductCustomization />
|
<ProductCustomization />
|
||||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
<div class="mb-6 w-[55%]">
|
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||||
<div class="flex justify-between items-center gap-10 mb-8">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||||
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||||
'px-15 py-2 cursor-pointer',
|
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!',
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
|||||||
89
bo/src/components/customer/PageProductsList.vue
Normal file
89
bo/src/components/customer/PageProductsList.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container mx-auto p-6">
|
||||||
|
<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">
|
||||||
|
Product ID
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Reference
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Price
|
||||||
|
</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 products" :key="product.product_id"
|
||||||
|
class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ product.product_id }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ product.reference }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ product.price.toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div v-if="products.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
No products found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
product_id: number
|
||||||
|
reference: string
|
||||||
|
price: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
items: Product[]
|
||||||
|
count: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const products = ref<Product[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function fetchProducts() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await useFetchJson<ApiResponse>('/api/v1/restricted/list-products/get-listing?p=3&elems') as unknown as { items: Product[] }
|
||||||
|
products.value = response.items || []
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || 'Failed to load products'
|
||||||
|
console.error('Error fetching products:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProducts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="text-[24px] font-bold">Product customization</p>
|
<p class="text-[24px] font-bold">Product customization</p>
|
||||||
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-10">
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-5 md:gap-10 ">
|
||||||
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
||||||
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
||||||
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TopBar from '@/components/TopBar.vue';
|
import TopBar from '@/components/TopBar.vue';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ function isAuthenticated(): boolean {
|
|||||||
if (typeof document === 'undefined') return false
|
if (typeof document === 'undefined') return false
|
||||||
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await getSettings()
|
await getSettings()
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.VITE_BASE_URL),
|
history: createWebHistory(import.meta.env.VITE_BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
@@ -34,6 +30,7 @@ const router = createRouter({
|
|||||||
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
||||||
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
||||||
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
|
{ path: 'cart', component: () => import('../components/customer/PageCart.vue'), name: 'cart' },
|
||||||
|
{ path: 'products-list', component: () => import('../components/customer/PageProductsList.vue'), name: 'products-list' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,7 +52,6 @@ const router = createRouter({
|
|||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const locale = to.params.locale as string
|
const locale = to.params.locale as string
|
||||||
const localeLang = langs.find((x) => x.iso_code == locale)
|
const localeLang = langs.find((x) => x.iso_code == locale)
|
||||||
|
|
||||||
if (locale && langs.length > 0) {
|
if (locale && langs.length > 0) {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
console.log(authStore.isAuthenticated, to, from)
|
console.log(authStore.isAuthenticated, to, from)
|
||||||
@@ -77,7 +73,6 @@ router.beforeEach((to, from, next) => {
|
|||||||
if (!locale && to.path !== '/') {
|
if (!locale && to.path !== '/') {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ export const useAddressStore = defineStore('address', () => {
|
|||||||
id: existing.id,
|
id: existing.id,
|
||||||
...normalize(formData)
|
...normalize(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
function deleteAddress(id: number): boolean {
|
function deleteAddress(id: number): boolean {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const PrivacyComponent = computed(() =>
|
|||||||
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
|
<UButton @click="showTherms = false" class="mx-auto px-12">{{ $t('general.close') }}</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
</UDrawer>
|
||||||
<!-- PrivacyPolicyView -->
|
|
||||||
<UDrawer v-model:open="showPrivacy" :overlay="false">
|
<UDrawer v-model:open="showPrivacy" :overlay="false">
|
||||||
<template #body>
|
<template #body>
|
||||||
<component :is="PrivacyComponent" />
|
<component :is="PrivacyComponent" />
|
||||||
@@ -76,9 +75,9 @@ const PrivacyComponent = computed(() =>
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
<UForm :validate="validate" @submit="handleLogin" class="space-y-5">
|
||||||
@@ -94,11 +93,12 @@ const PrivacyComponent = computed(() =>
|
|||||||
|
|
||||||
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
|
<UFormField :label="$t('general.password')" name="password" required class="w-full dark:text-white text-black">
|
||||||
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
|
<UInput v-model="password" :placeholder="$t('general.enter_your_password')"
|
||||||
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)" :ui="{ trailing: 'pe-1' }">
|
:type="showPassword ? 'text' : 'password'" class="w-full placeholder:text-(--placeholder)"
|
||||||
|
:ui="{ trailing: 'pe-1' }">
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
<UIcon color="neutral" variant="link" size="sm" :name="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||||
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
|
:aria-label="showPassword ? 'Hide password' : 'Show password'" :aria-pressed="showPassword"
|
||||||
aria-controls="password" @click="showPassword = !showPassword" class="mr-2"/>
|
aria-controls="password" @click="showPassword = !showPassword" class="mr-2" />
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
@@ -114,15 +114,11 @@ const PrivacyComponent = computed(() =>
|
|||||||
{{ $t('general.sign_in') }}
|
{{ $t('general.sign_in') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="flex items-center gap-3 my-1">
|
<div class="flex items-center gap-3 my-1">
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Google Sign In -->
|
|
||||||
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
|
<UButton type="button" color="neutral" variant="outline" size="lg" block :disabled="authStore.loading"
|
||||||
@click="authStore.loginWithGoogle()"
|
@click="authStore.loginWithGoogle()"
|
||||||
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
class="flex items-center justify-center gap-2 dark:text-white text-black cursor-pointer">
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ function validate(): FormError[] {
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md">
|
<div class="w-full max-w-md">
|
||||||
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
|
<UForm :validate="validate" @submit="handleRegister" class="flex flex-col gap-3">
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container mx-auto">
|
||||||
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
||||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ function validate(): FormError[] {
|
|||||||
<div class="text-center mb-15">
|
<div class="text-center mb-15">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col gap-4">
|
<div class="w-full max-w-md flex flex-col gap-4">
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ function goToLogin() {
|
|||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30">
|
||||||
<UIcon name="i-heroicons-clock" class="w-8 h-8" />
|
<UIcon name="carbon:ibm-webmethods-b2b-integration" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">TimeTracker</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">B2B</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
<UCard class="shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50">
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ CREATE TABLE IF NOT EXISTS b2b_tracker_routes (
|
|||||||
path VARCHAR(255) NULL,
|
path VARCHAR(255) NULL,
|
||||||
component VARCHAR(255) NOT NULL COMMENT 'path to component file',
|
component VARCHAR(255) NOT NULL COMMENT 'path to component file',
|
||||||
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
|
layout VARCHAR(50) DEFAULT 'default' COMMENT "'default' | 'empty'",
|
||||||
meta JSON DEFAULT '{}',
|
meta JSON DEFAULT '{}' ,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
sort_order INT DEFAULT 0,
|
sort_order INT DEFAULT 0,
|
||||||
parent_id INT NULL,
|
parent_id INT NULL
|
||||||
|
|
||||||
CONSTRAINT fk_parent
|
|
||||||
FOREIGN KEY (parent_id)
|
|
||||||
REFERENCES b2b_tracker_routes(id)
|
|
||||||
ON DELETE SET NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE b2b_tracker_routes
|
||||||
|
ADD CONSTRAINT fk_parent
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES b2b_tracker_routes(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
INSERT IGNORE INTO b2b_tracker_routes
|
INSERT IGNORE INTO b2b_tracker_routes
|
||||||
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
(name, path, component, layout, meta, is_active, sort_order, parent_id)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_to
|
|||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- insert sample admin user admin@ma-al.com/Maal12345678
|
||||||
|
|
||||||
|
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
|
||||||
|
VALUES
|
||||||
|
(1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 'admin', 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 1, 1, '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL);
|
||||||
|
ALTER TABLE b2b_customers AUTO_INCREMENT = 1;
|
||||||
|
|
||||||
-- countries
|
-- countries
|
||||||
CREATE TABLE IF NOT EXISTS b2b_countries (
|
CREATE TABLE IF NOT EXISTS b2b_countries (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package categoriesRepo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UICategoriesRepo interface {
|
|
||||||
GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoriesRepo struct{}
|
|
||||||
|
|
||||||
func New() UICategoriesRepo {
|
|
||||||
return &CategoriesRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *CategoriesRepo) GetAllCategories(id_shop uint, id_lang uint) ([]model.ScannedCategory, error) {
|
|
||||||
var allCategories []model.ScannedCategory
|
|
||||||
|
|
||||||
err := db.DB.Raw(`
|
|
||||||
SELECT
|
|
||||||
ps_category.id_category AS ID,
|
|
||||||
ps_category_lang.name AS name,
|
|
||||||
ps_category.active AS active,
|
|
||||||
ps_category_shop.position AS position,
|
|
||||||
ps_category.id_parent AS id_parent,
|
|
||||||
ps_category.is_root_category AS is_root_category
|
|
||||||
FROM ps_category
|
|
||||||
LEFT JOIN ps_category_lang
|
|
||||||
ON ps_category_lang.id_category = ps_category.id_category
|
|
||||||
AND ps_category_lang.id_shop = ?
|
|
||||||
AND ps_category_lang.id_lang = ?
|
|
||||||
LEFT JOIN ps_category_shop
|
|
||||||
ON ps_category_shop.id_category = ps_category.id_category
|
|
||||||
AND ps_category_shop.id_shop = ?`,
|
|
||||||
id_shop, id_lang, id_shop).
|
|
||||||
Scan(&allCategories).Error
|
|
||||||
|
|
||||||
return allCategories, err
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,15 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi
|
|||||||
var listing []model.ProductInList
|
var listing []model.ProductInList
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
|
// Apply filters here
|
||||||
|
q := db.DB.Table("ps_product").
|
||||||
|
Select("ps_product.id_product AS id_product", "ps_product_lang.name AS name", "ps_product_shop.price AS price", "ps_product.active AS active_as_product", "ps_product_shop.active AS active_in_shop").
|
||||||
|
Joins("LEFT JOIN ps_product_shop ON ps_product.id_product = ps_product_shop.id_product").
|
||||||
|
Joins("LEFT JOIN ps_product_lang ON ps_product.id_product = ps_product_lang.id_product").
|
||||||
|
Where("ps_product_shop.id_shop = ?", id_shop).
|
||||||
|
Where("ps_product_lang.id_shop = ?", id_shop).
|
||||||
|
Where("ps_product_lang.id_lang = ?", id_lang)
|
||||||
|
|
||||||
// var resultIDs []uint
|
// var resultIDs []uint
|
||||||
// q := db.DB.
|
// q := db.DB.
|
||||||
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
||||||
@@ -35,41 +44,15 @@ func (repo *ListProductsRepo) GetListing(id_shop uint, id_lang uint, p find.Pagi
|
|||||||
// Limit(p.Limit()).
|
// Limit(p.Limit()).
|
||||||
// Offset(p.Offset())
|
// Offset(p.Offset())
|
||||||
|
|
||||||
err := db.DB.Raw(`
|
err := q.Count(&total).Error
|
||||||
SELECT
|
|
||||||
ps_product.id_product AS ID,
|
|
||||||
ps_product_lang.name AS name,
|
|
||||||
ps_product.active AS active,
|
|
||||||
ps_product_lang.link_rewrite AS link_rewrite,
|
|
||||||
COALESCE (
|
|
||||||
ps_image_shop.id_image, any_image.id_image
|
|
||||||
) AS id_image
|
|
||||||
FROM ps_product
|
|
||||||
LEFT JOIN ps_product_lang
|
|
||||||
ON ps_product_lang.id_product = ps_product.id_product
|
|
||||||
AND ps_product_lang.id_shop = ?
|
|
||||||
AND ps_product_lang.id_lang = ?
|
|
||||||
LEFT JOIN ps_image_shop
|
|
||||||
ON ps_image_shop.id_product = ps_product.id_product
|
|
||||||
AND ps_image_shop.id_shop = ?
|
|
||||||
AND ps_image_shop.cover = 1
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT id_product, MIN(id_image) AS id_image
|
|
||||||
FROM ps_image
|
|
||||||
GROUP BY id_product
|
|
||||||
) any_image
|
|
||||||
ON ps_product.id_product = any_image.id_product
|
|
||||||
LIMIT ? OFFSET ?`,
|
|
||||||
id_shop, id_lang, id_shop, p.Limit(), p.Offset()).
|
|
||||||
Scan(&listing).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return find.Found[model.ProductInList]{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.Raw(`
|
err = q.
|
||||||
SELECT COUNT(*)
|
Limit(p.Limit()).
|
||||||
FROM ps_product`).
|
Offset(p.Offset()).
|
||||||
Scan(&total).Error
|
Scan(&listing).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return find.Found[model.ProductInList]{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user