fix: sorting and filtering
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<suspense>
|
||||
<component :is="Default || 'div'">
|
||||
<!-- <div class="w-64 h-128">
|
||||
<CategoryMenu />
|
||||
</div> -->
|
||||
|
||||
<div class="container mx-auto mt-20">
|
||||
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
|
||||
<template #item="{ item, active }">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<UIcon name="i-heroicons-book-open" />
|
||||
<span>{{ item.name }}</span>
|
||||
</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>
|
||||
@@ -14,42 +19,13 @@
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Image</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Product Code</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Name</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="product in productsList" :key="product.product_id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
@click="goToProduct(product.product_id)">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<img :src="product.image_link" alt="product image"
|
||||
class="w-16 h-16 object-cover rounded" />
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||
product.reference }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{
|
||||
product.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600 dark:text-blue-400">
|
||||
{{ product.link_rewrite }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList" :columns="columnsChild" :ui="{
|
||||
thead: 'hidden'
|
||||
}" />
|
||||
</template>
|
||||
</UTable>
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||
</div>
|
||||
@@ -63,21 +39,65 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ref, onMounted, watch, h, resolveComponent, computed } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import { log } from 'console'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id:number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const page = ref(1)
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page) || 1,
|
||||
set: (val: number) => {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
page: val
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const sortField = computed({
|
||||
get: () => [
|
||||
route.query.sort as string | undefined,
|
||||
route.query.direction as 'asc' | 'desc' | undefined
|
||||
],
|
||||
|
||||
set: ([sort]: [string, 'asc' | 'desc']) => {
|
||||
const currentSort = route.query.sort as string | undefined
|
||||
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
|
||||
|
||||
let query = { ...route.query }
|
||||
|
||||
if (currentSort === sort) {
|
||||
if (currentDirection === 'asc') {
|
||||
query.direction = 'desc'
|
||||
} else if (currentDirection === 'desc') {
|
||||
delete query.sort
|
||||
delete query.direction
|
||||
} else {
|
||||
query.direction = 'asc'
|
||||
query.sort = sort
|
||||
}
|
||||
} else {
|
||||
query.sort = sort
|
||||
query.direction = 'asc'
|
||||
}
|
||||
|
||||
router.push({ query })
|
||||
}
|
||||
})
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
@@ -90,18 +110,33 @@ interface ApiResponse {
|
||||
const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = ref<Record<string, string>>({})
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const [sort, direction] = sortField.value
|
||||
|
||||
const params = new URLSearchParams({
|
||||
p: String(page.value),
|
||||
elems: String(perPage.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()}`
|
||||
|
||||
try {
|
||||
const response = await useFetchJson(
|
||||
`api/v1/restricted/list-products/get-listing?p=${page.value}&elems=${perPage.value}`
|
||||
) as ApiResponse
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
|
||||
productsList.value = response.items || []
|
||||
console.log(response)
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
@@ -110,15 +145,255 @@ async function fetchProductList() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(page, () => {
|
||||
fetchProductList()
|
||||
})
|
||||
onMounted(fetchProductList)
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
})
|
||||
}
|
||||
|
||||
const selectedCount = ref({
|
||||
product_id: null,
|
||||
count: 0
|
||||
})
|
||||
|
||||
function getIcon(name: string) {
|
||||
if (sortField.value[0] === name) {
|
||||
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
||||
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
|
||||
}
|
||||
return 'i-lucide-arrow-up-down'
|
||||
}
|
||||
|
||||
const UInputNumber = resolveComponent('UInputNumber')
|
||||
const UInput = resolveComponent('UInput')
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
{
|
||||
id: 'expand',
|
||||
cell: ({ row }) =>
|
||||
h(UButton, {
|
||||
color: 'neutral',
|
||||
variant: 'ghost',
|
||||
icon: 'i-lucide-chevron-down',
|
||||
square: true,
|
||||
'aria-label': 'Expand',
|
||||
ui: {
|
||||
leadingIcon: [
|
||||
'transition-transform',
|
||||
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
|
||||
]
|
||||
},
|
||||
onClick: () => row.toggleExpanded()
|
||||
})
|
||||
},
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['product_id', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'ID'),
|
||||
h(UIcon, {
|
||||
name: getIcon('product_id')
|
||||
})
|
||||
]),
|
||||
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: column.getFilterValue() ?? '',
|
||||
'onUpdate:modelValue': (val: string) => {
|
||||
if (val) {
|
||||
filters.value[column.id] = val
|
||||
} else {
|
||||
delete filters.value[column.id]
|
||||
}
|
||||
},
|
||||
size: 'xs'
|
||||
})
|
||||
])
|
||||
},
|
||||
// header: '#',
|
||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'image_link',
|
||||
header: 'Image',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image_link') as string,
|
||||
style: 'width:40px;height:40px;object-fit:cover;'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['name', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'Name'),
|
||||
h(UIcon, {
|
||||
name: getIcon('name')
|
||||
})
|
||||
]),
|
||||
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: column.getFilterValue() ?? '',
|
||||
'onUpdate:modelValue': (val: string) => {
|
||||
if (val) {
|
||||
filters.value[column.id] = val
|
||||
} else {
|
||||
delete filters.value[column.id]
|
||||
}
|
||||
},
|
||||
size: 'xs'
|
||||
})
|
||||
])
|
||||
},
|
||||
cell: ({ row }) => row.getValue('name') as string,
|
||||
filterFn: (row, columnId, value) => {
|
||||
const name = row.getValue(columnId) as string
|
||||
return name.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: ({ column }) => {
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => {
|
||||
sortField.value = ['quantity', 'asc']
|
||||
}
|
||||
}, [
|
||||
h('span', 'In stock'),
|
||||
h(UIcon, {
|
||||
name: getIcon('quantity')
|
||||
})
|
||||
]),
|
||||
])
|
||||
},
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: 'Count',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const columnsChild: TableColumn<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'product_id',
|
||||
header: '',
|
||||
cell: ({ row }) => `#${row.getValue('product_id') as number}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'image_link',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image_link') as string,
|
||||
style: 'width:40px;height:40px;object-fit:cover;'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('name') as string
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: '',
|
||||
cell: ({ row }) => row.getValue('quantity') as number
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UInputNumber, {
|
||||
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
|
||||
'onUpdate:modelValue': (val: number) => {
|
||||
if (val)
|
||||
selectedCount.value = {
|
||||
product_id: row.original.product_id,
|
||||
count: val
|
||||
}
|
||||
else {
|
||||
selectedCount.value = {
|
||||
product_id: null,
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: row.original.quantity
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
console.log('Clicked', row.original)
|
||||
},
|
||||
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
|
||||
disabled: selectedCount.value.product_id !== row.original.product_id,
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, sortField, filters.value], () => {
|
||||
fetchProductList()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
Reference in New Issue
Block a user