fix: page usersList
This commit is contained in:
@@ -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>
|
||||
25
bo/src/components/admin/UsersSearch.vue
Normal file
25
bo/src/components/admin/UsersSearch.vue
Normal 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>
|
||||
11
bo/src/components/customer-management/Profile.vue
Normal file
11
bo/src/components/customer-management/Profile.vue
Normal 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>
|
||||
Reference in New Issue
Block a user