fix: page usersList

This commit is contained in:
2026-04-07 16:06:36 +02:00
parent a290a72d1d
commit 9eb8fc6625
7 changed files with 500 additions and 201 deletions

View File

@@ -3,62 +3,47 @@
<div class="flex flex-col md:flex-row gap-10">
<CategoryMenu />
<div class="w-full flex flex-col items-center gap-4">
<UTable :data="usersList" :columns="columns" class="flex-1 w-full"
:ui="{ root: 'max-w-100wv overflow-auto!' }" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div>
</div>
</component>
</template>
<script setup lang="ts">
import Default from '@/layouts/default.vue';
import { computed, onMounted, ref, resolveComponent, h } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson';
import type { TableColumn } from '@nuxt/ui';
import type { Product } from '@/types/product';
import { useRoute, useRouter } from 'vue-router';
const usersList = ref([])
const error = ref()
async function fetchUsersList() {
error.value = null
try {
const data = await useFetchJson(`/api/v1/restricted/customer/list`)
console.log('USERS LIST:', data)
const response = (data as any).items ?? data
console.log('User list response:', response)
usersList.value = response
return response
} catch (err: any) {
error.value = err?.message ?? 'Unknown error'
return null
}
}
onMounted(() => {
fetchUsersList()
})
import Default from '@/layouts/default.vue'
import { ref, computed, watch, resolveComponent, h } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import { useRoute, useRouter } from 'vue-router'
import CategoryMenu from '../inner/CategoryMenu.vue'
import type { TableColumn } from '@nuxt/ui'
import type { Customer } from '@/types/user'
const router = useRouter()
const route = useRoute()
const perPage = ref(15)
const page = computed({
get: () => Number(route.query.p) || 1,
set: (val: number) => {
router.push({ query: { ...route.query, p: 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 }
const query = { ...route.query }
if (currentSort === sort) {
if (currentDirection === 'asc') {
query.direction = 'desc'
} else if (currentDirection === 'desc') {
if (currentDirection === 'asc') query.direction = 'desc'
else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
@@ -69,33 +54,10 @@ const sortField = computed({
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
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 updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
@@ -106,160 +68,145 @@ const filters = computed<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
}
Object.keys(baseQuery).forEach(k => {
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
})
router.push({ query: { ...baseQuery, ...val, page: 1 } })
}
})
import errorImg from '@/assets/error.svg'
import { debounce } from 'chart.js/helpers';
const columns: TableColumn<Product>[] = [
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)
const usersList = ref<Customer[]>([])
const total = ref(0)
const loading = ref(true)
const error = ref<string | null>(null)
async function fetchUsersList() {
loading.value = true
error.value = null
const params = new URLSearchParams(route.query as any).toString()
const url = `/api/v1/restricted/customer/list?elems=${perPage.value}&${params}`
try {
const res = await useFetchJson<{ items: { items: Customer[] }; count: number }>(url)
usersList.value = res.items?.items || []
total.value = res.count || 0
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load users'
} finally {
loading.value = false
}
}
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 UInput = resolveComponent('UInput')
const UIcon = resolveComponent('UIcon')
const UButton = resolveComponent('UButton')
const columns: TableColumn<Customer>[] = [
{
accessorKey: 'user_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 = ['user_id', 'asc']
}
}, [
h('span', 'Client ID'),
h(UIcon, {
name: getIcon('user_id')
})
]),
])
},
// header: '#',
cell: ({ row }) => `#${row.getValue('user_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;',
// onError: (e: Event) => {
// const target = e.target as HTMLImageElement
// target.src = errorImg
// }
// })
// },
// },
{
accessorKey: 'company_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 = ['company_name', 'asc']
}
}, [
h('span', 'Company Name'),
h(UIcon, {
name: getIcon('name')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => row.getValue('company_name') as string,
filterFn: (row, columnId, value) => {
const name = row.getValue(columnId) as string
return name.toLowerCase().includes(value.toLowerCase())
}
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['user_id', 'asc'])
}, [h('span', 'Client ID'), h(UIcon, { name: getIcon('user_id') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `#${row.getValue('user_id')}`
},
{
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/Surname'),
h(UIcon, {
name: getIcon('quantity')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => row.getValue('name') as string
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['last_name, first_name', 'asc'])
}, [h('span', 'Name/Surname'), h(UIcon, { name: getIcon('name') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`
},
{
accessorKey: 'email',
header: ({ column }) => {
return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['email', 'asc']
}
}, [
h('span', 'Email'),
h(UIcon, {
name: getIcon('quantity')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => row.getValue('email') as string
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => (sortField.value = ['email', 'asc'])
}, [h('span', 'Email'), h(UIcon, { name: getIcon('email') })]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
size: 'xs'
})
]),
cell: ({ row }) => row.getValue('email')
},
// {
// accessorKey: 'count',
// header: '',
// cell: ({ row }) => {
// return h(UButton, {
// onClick: () => {
// goToProduct(row.original.product_id, row.original.link_rewrite)
// },
// class: 'cursor-pointer',
// color: 'info',
// variant: 'soft'
// }, () => 'Show product')
// },
// }
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
// onClick: () => {
// goToProduct(row.original.product_id, row.original.link_rewrite)
// },
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Manage')
},
},
{
accessorKey: 'profile',
header: '',
cell: ({ row }) => {
const userId = row.original.user_id
return h(UButton, {
color: 'primary',
size: 'sm',
variant: 'soft',
onClick: () => {
router.push({
name: 'customer-management-profile',
params: { user_id: userId }
})
}
}, () => 'Go to profile')
}
}
]
watch(() => route.query, fetchUsersList, { immediate: true })
</script>

View File

@@ -0,0 +1,25 @@
<template>
<component :is="Default || 'div'">
<div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)">
<h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search User</h1>
<div class="w-full max-w-4xl">
<input type="text" placeholder="Type user name or ID..."
class="w-full border border-gray-300 rounded-full px-6 py-3 text-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
</div>
</component>
</template>
<script setup lang="ts">
import Default from '@/layouts/default.vue';
</script>
<style scoped>
input::placeholder {
color: #9ca3af;
/* Tailwind gray-400 */
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<component :is="Management || 'div'">
<div>customer-management</div>
</component>
</template>
<script setup lang="ts">
import Management from '@/layouts/management.vue';
</script>