fix: routing/data table

This commit is contained in:
2026-03-26 15:55:35 +01:00
parent 21bea39e46
commit 3246ef4fb7
16 changed files with 411 additions and 558 deletions

View File

@@ -10,7 +10,6 @@
</div>
</template>
</UNavigationMenu> -->
{{ filters }}
<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>
@@ -19,13 +18,16 @@
{{ error }}
</div>
<div v-else class="overflow-x-auto">
<UTable :data="productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }">
<UTable :data="productsList" :columns="columnsChild" :ui="{
thead: 'hidden'
}" />
</template>
</UTable>
<div class="flex gap-2">
<CategoryMenuListing />
<UTable :data="productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }">
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
thead: 'hidden'
}" />
</template>
</UTable>
</div>
<div class="flex justify-center items-center py-8">
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
</div>
@@ -39,12 +41,12 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, h, resolveComponent, computed } from 'vue'
import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui'
import { log } from 'console'
import CategoryMenuListing from '../inner/categoryMenuListing.vue'
interface Product {
reference: number
@@ -98,6 +100,7 @@ const sortField = computed({
router.push({ query })
}
})
const perPage = ref(15)
const total = ref(0)
@@ -110,32 +113,64 @@ interface ApiResponse {
const productsList = ref<Product[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const filters = ref<Record<string, string>>({})
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(key => {
if (!['page', 'sort', 'direction'].includes(key)) {
delete baseQuery[key]
}
})
router.push({
query: {
...baseQuery,
...val,
page: 1
}
})
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
async function fetchProductList() {
loading.value = true
error.value = null
const [sort, direction] = sortField.value
const params = new URLSearchParams()
const params = new URLSearchParams({
p: String(page.value),
elems: String(perPage.value)
Object.entries(route.query).forEach(([key, value]) => {
if (value) params.append(key, String(value))
})
if (sort && direction) {
params.append('sort', `${sort},${direction}`)
}
Object.entries(filters.value).forEach(([key, value]) => {
if (value) params.append(key, value)
})
const url = `/api/v1/restricted/list-products/get-listing?${params.toString()}`
const url = `/api/v1/restricted/list-products/get-listing?${params}`
try {
const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || []
total.value = response.count || 0
} catch (e: unknown) {
@@ -207,13 +242,9 @@ const columns: TableColumn<Payment>[] = [
h(UInput, {
placeholder: 'Search...',
modelValue: column.getFilterValue() ?? '',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
if (val) {
filters.value[column.id] = val
} else {
delete filters.value[column.id]
}
updateFilter(column.id, val)
},
size: 'xs'
})
@@ -250,13 +281,9 @@ const columns: TableColumn<Payment>[] = [
h(UInput, {
placeholder: 'Search...',
modelValue: column.getFilterValue() ?? '',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
if (val) {
filters.value[column.id] = val
} else {
delete filters.value[column.id]
}
updateFilter(column.id, val)
},
size: 'xs'
})
@@ -393,7 +420,11 @@ const columnsChild: TableColumn<Payment>[] = [
}
]
watch([page, sortField, filters.value], () => {
fetchProductList()
}, { immediate: true })
watch(
() => route.query,
() => {
fetchProductList()
},
{ immediate: true }
)
</script>

View File

@@ -1,5 +1,4 @@
<template>
<component :is="Default || 'div'">
<div class="container my-10 mx-auto ">

View File

@@ -1,243 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue'
import Default from '@/layouts/default.vue'
const router = useRouter()
const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n()
const searchName = ref('')
const searchCode = ref('')
const priceFromFilter = ref<number | null>(null)
const priceToFilter = ref<number | null>(null)
// Pagination
const page = ref(1)
const pageSize = 5
// Fetch products on mount
// onMounted(() => {
// productStore.getProductDescription(langID: , productID.value)
// })
// Filtered products
// const filteredProducts = computed(() => {
// console.log(productStore.products);
// return productStore.products.filter(product => {
// const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
// const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
// const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
// const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
// return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
// })
// })
// const totalItems = computed(() => filteredProducts.value.length)
// const paginatedProducts = computed(() => {
// const start = (page.value - 1) * pageSize
// const end = start + pageSize
// return filteredProducts.value.slice(start, end)
// })
// Reset page when filters change
function resetPage() {
page.value = 1
}
// Navigate to product detail
function goToProduct(product: Product) {
router.push({ name: 'product-detail', params: { id: product.id } })
}
// Table columns
const columns = computed<TableColumn<Product>[]>(() => [
{
accessorKey: 'image',
header: () => h('div', { class: 'text-center' }, t('products.image')),
cell: ({ row }) => h('img', {
src: row.getValue('image'),
alt: 'Product',
class: 'w-12 h-12 object-cover rounded'
})
},
{
accessorKey: 'name',
header: t('products.product_name'),
cell: ({ row }) => {
const product = row.original
return h('button', {
class: 'text-primary hover:underline font-medium text-left',
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
}, product.name)
}
},
{
accessorKey: 'code',
header: t('products.product_code'),
},
{
accessorKey: 'description',
header: t('products.description'),
cell: ({ row }) => {
const desc = row.getValue('description') as string
return h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, desc?.substring(0, 50) + (desc && desc.length > 50 ? '...' : ''))
}
},
{
accessorKey: 'inStock',
header: t('products.in_stock'),
cell: ({ row }) => {
const inStock = row.getValue('inStock')
return h('span', {
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
}, inStock ? t('products.yes') : t('products.no'))
}
},
{
accessorKey: 'price',
header: t('products.price'),
cell: ({ row }) => {
const priceFromVal = row.original.priceFrom
const priceToVal = row.original.priceTo
return `${priceFromVal} - ${priceToVal}`
}
},
{
accessorKey: 'count',
header: t('products.count'),
},
{
id: 'actions',
header: '',
cell: ({ row }) => {
const product = row.original
return h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
}, t('products.add_to_cart')),
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
}, '+')
])
}
}
])
// Actions
function addToCart(product: Product) {
console.log('Add to cart:', product)
}
function incrementCount(product: Product) {
product.count++
}
function clearFilters() {
searchName.value = ''
searchCode.value = ''
priceFromFilter.value = null
priceToFilter.value = null
resetPage()
}
</script>
<template>
<component :is="Default || 'div'">
<div class="container">
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<div>
<!-- v-html="productStore.products" -->
</div>
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
{{ t('products.login_to_view') }}
</div>
<!-- Loading State -->
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<!-- Error State -->
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
<!-- Filter Block -->
<div
class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name')
}}</label>
<UInput v-model="searchName" :placeholder="t('products.search_name_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code')
}}</label>
<UInput v-model="searchCode" :placeholder="t('products.search_code_placeholder')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
<UInput v-model="priceFromFilter" type="number" :placeholder="t('products.price_from')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
<UInput v-model="priceToFilter" type="number" :placeholder="t('products.price_to')"
@update:model-value="resetPage" class="dark:text-white text-black" />
</div>
<div class="flex items-end">
<button @click="clearFilters"
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
{{ t('products.clear_filters') }}
</button>
</div>
</div>
<!-- Products Table -->
<!-- <div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
<UTable
:data="paginatedProducts"
:columns="columns"
class="dark:text-white! text-dark"
/>
</div> -->
<!-- Empty State -->
<!-- <div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
{{ t('products.no_products') }}
</div> -->
<!-- Pagination -->
<!-- <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination
v-model:page="page"
:page-count="pageSize"
:total="totalItems"
/>
</div> -->
<!-- Results count -->
<!-- <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
{{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
</div> -->
</div>
</div>
</div>
</component>
</template>