fix: page usersList
This commit is contained in:
3
bo/components.d.ts
vendored
3
bo/components.d.ts
vendored
@@ -11,6 +11,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ButtonGoToProfile: typeof import('./src/components/customer-management/ButtonGoToProfile.vue')['default']
|
||||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||||
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
|
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
|
||||||
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
|
||||||
@@ -36,6 +37,7 @@ declare module 'vue' {
|
|||||||
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
||||||
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
|
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
|
||||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||||
|
Profile: typeof import('./src/components/customer-management/Profile.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
|
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
|
||||||
@@ -61,6 +63,7 @@ declare module 'vue' {
|
|||||||
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||||
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||||
UsersList: typeof import('./src/components/admin/UsersList.vue')['default']
|
UsersList: typeof import('./src/components/admin/UsersList.vue')['default']
|
||||||
|
UsersSearch: typeof import('./src/components/admin/UsersSearch.vue')['default']
|
||||||
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
|
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
|
||||||
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
||||||
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||||
|
|||||||
@@ -3,62 +3,47 @@
|
|||||||
<div class="flex flex-col md:flex-row gap-10">
|
<div class="flex flex-col md:flex-row gap-10">
|
||||||
<CategoryMenu />
|
<CategoryMenu />
|
||||||
<div class="w-full flex flex-col items-center gap-4">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Default from '@/layouts/default.vue';
|
import Default from '@/layouts/default.vue'
|
||||||
import { computed, onMounted, ref, resolveComponent, h } from 'vue'
|
import { ref, computed, watch, resolveComponent, h } from 'vue'
|
||||||
import { useFetchJson } from '@/composable/useFetchJson';
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
import type { TableColumn } from '@nuxt/ui';
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { Product } from '@/types/product';
|
import CategoryMenu from '../inner/CategoryMenu.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
|
import type { Customer } from '@/types/user'
|
||||||
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()
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
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({
|
const sortField = computed({
|
||||||
get: () => [
|
get: () => [
|
||||||
route.query.sort as string | undefined,
|
route.query.sort as string | undefined,
|
||||||
route.query.direction as 'asc' | 'desc' | undefined
|
route.query.direction as 'asc' | 'desc' | undefined
|
||||||
],
|
],
|
||||||
|
|
||||||
set: ([sort]: [string, 'asc' | 'desc']) => {
|
set: ([sort]: [string, 'asc' | 'desc']) => {
|
||||||
const currentSort = route.query.sort as string | undefined
|
const currentSort = route.query.sort as string | undefined
|
||||||
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
|
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
|
||||||
|
const query = { ...route.query }
|
||||||
let query = { ...route.query }
|
|
||||||
|
|
||||||
if (currentSort === sort) {
|
if (currentSort === sort) {
|
||||||
if (currentDirection === 'asc') {
|
if (currentDirection === 'asc') query.direction = 'desc'
|
||||||
query.direction = 'desc'
|
else if (currentDirection === 'desc') {
|
||||||
} else if (currentDirection === 'desc') {
|
|
||||||
delete query.sort
|
delete query.sort
|
||||||
delete query.direction
|
delete query.direction
|
||||||
} else {
|
} else {
|
||||||
@@ -69,33 +54,10 @@ const sortField = computed({
|
|||||||
query.sort = sort
|
query.sort = sort
|
||||||
query.direction = 'asc'
|
query.direction = 'asc'
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({ query })
|
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>>({
|
const filters = computed<Record<string, string>>({
|
||||||
get: () => {
|
get: () => {
|
||||||
const q = { ...route.query }
|
const q = { ...route.query }
|
||||||
@@ -106,160 +68,145 @@ const filters = computed<Record<string, string>>({
|
|||||||
},
|
},
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
const baseQuery = { ...route.query }
|
const baseQuery = { ...route.query }
|
||||||
|
Object.keys(baseQuery).forEach(k => {
|
||||||
Object.keys(baseQuery).forEach(key => {
|
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
|
||||||
if (!['page', 'sort', 'direction'].includes(key)) {
|
})
|
||||||
delete baseQuery[key]
|
router.push({ query: { ...baseQuery, ...val, page: 1 } })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
function debounce(fn: Function, delay = 400) {
|
||||||
query: {
|
let t: any
|
||||||
...baseQuery,
|
return (...args: any[]) => {
|
||||||
...val,
|
clearTimeout(t)
|
||||||
page: 1
|
t = setTimeout(() => fn(...args), delay)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
import errorImg from '@/assets/error.svg'
|
const updateFilter = debounce((columnId: string, val: string) => {
|
||||||
import { debounce } from 'chart.js/helpers';
|
const newFilters = { ...filters.value }
|
||||||
const columns: TableColumn<Product>[] = [
|
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',
|
accessorKey: 'user_id',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
|
||||||
h('div', {
|
h('div', {
|
||||||
class: 'flex items-center gap-2 cursor-pointer',
|
class: 'flex items-center gap-2 cursor-pointer',
|
||||||
onClick: () => {
|
onClick: () => (sortField.value = ['user_id', 'asc'])
|
||||||
sortField.value = ['user_id', 'asc']
|
}, [h('span', 'Client ID'), h(UIcon, { name: getIcon('user_id') })]),
|
||||||
}
|
|
||||||
}, [
|
|
||||||
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, {
|
h(UInput, {
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
modelValue: filters.value[column.id] ?? '',
|
modelValue: filters.value[column.id] ?? '',
|
||||||
'onUpdate:modelValue': (val: string) => {
|
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||||
updateFilter(column.id, val)
|
|
||||||
},
|
|
||||||
size: 'xs'
|
size: 'xs'
|
||||||
})
|
})
|
||||||
])
|
]),
|
||||||
},
|
cell: ({ row }) => `#${row.getValue('user_id')}`
|
||||||
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())
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
|
||||||
h('div', {
|
h('div', {
|
||||||
class: 'flex items-center gap-2 cursor-pointer',
|
class: 'flex items-center gap-2 cursor-pointer',
|
||||||
onClick: () => {
|
onClick: () => (sortField.value = ['last_name, first_name', 'asc'])
|
||||||
sortField.value = ['name', 'asc']
|
}, [h('span', 'Name/Surname'), h(UIcon, { name: getIcon('name') })]),
|
||||||
}
|
|
||||||
}, [
|
|
||||||
h('span', 'Name/Surname'),
|
|
||||||
h(UIcon, {
|
|
||||||
name: getIcon('quantity')
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
h(UInput, {
|
h(UInput, {
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
modelValue: filters.value[column.id] ?? '',
|
modelValue: filters.value[column.id] ?? '',
|
||||||
'onUpdate:modelValue': (val: string) => {
|
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||||
updateFilter(column.id, val)
|
|
||||||
},
|
|
||||||
size: 'xs'
|
size: 'xs'
|
||||||
})
|
})
|
||||||
])
|
]),
|
||||||
},
|
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`
|
||||||
cell: ({ row }) => row.getValue('name') as string
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
|
||||||
h('div', {
|
h('div', {
|
||||||
class: 'flex items-center gap-2 cursor-pointer',
|
class: 'flex items-center gap-2 cursor-pointer',
|
||||||
onClick: () => {
|
onClick: () => (sortField.value = ['email', 'asc'])
|
||||||
sortField.value = ['email', 'asc']
|
}, [h('span', 'Email'), h(UIcon, { name: getIcon('email') })]),
|
||||||
}
|
|
||||||
}, [
|
|
||||||
h('span', 'Email'),
|
|
||||||
h(UIcon, {
|
|
||||||
name: getIcon('quantity')
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
h(UInput, {
|
h(UInput, {
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
modelValue: filters.value[column.id] ?? '',
|
modelValue: filters.value[column.id] ?? '',
|
||||||
'onUpdate:modelValue': (val: string) => {
|
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||||
updateFilter(column.id, val)
|
|
||||||
},
|
|
||||||
size: 'xs'
|
size: 'xs'
|
||||||
})
|
})
|
||||||
])
|
]),
|
||||||
|
cell: ({ row }) => row.getValue('email')
|
||||||
},
|
},
|
||||||
cell: ({ row }) => row.getValue('email') as string
|
{
|
||||||
},
|
accessorKey: 'count',
|
||||||
// {
|
header: '',
|
||||||
// accessorKey: 'count',
|
cell: ({ row }) => {
|
||||||
// header: '',
|
return h(UButton, {
|
||||||
// cell: ({ row }) => {
|
|
||||||
// return h(UButton, {
|
|
||||||
// onClick: () => {
|
// onClick: () => {
|
||||||
// goToProduct(row.original.product_id, row.original.link_rewrite)
|
// goToProduct(row.original.product_id, row.original.link_rewrite)
|
||||||
// },
|
// },
|
||||||
// class: 'cursor-pointer',
|
class: 'cursor-pointer',
|
||||||
// color: 'info',
|
color: 'info',
|
||||||
// variant: 'soft'
|
variant: 'soft'
|
||||||
// }, () => 'Show product')
|
}, () => '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>
|
</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>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 p-4 bg-slate-50">
|
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
305
bo/src/layouts/management.vue
Normal file
305
bo/src/layouts/management.vue
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 overflow-x-hidden h-svh">
|
||||||
|
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
||||||
|
container: 'h-full z-80',
|
||||||
|
inner: 'bg-elevated/25 divide-transparent',
|
||||||
|
body: 'py-0'
|
||||||
|
}">
|
||||||
|
<template #header>
|
||||||
|
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
|
||||||
|
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||||
|
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
|
||||||
|
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||||
|
trailingIcon: 'text-dimmed ms-auto'
|
||||||
|
}" />
|
||||||
|
</UDropdownMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ state }">
|
||||||
|
{{ menu }}
|
||||||
|
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
||||||
|
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
||||||
|
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||||
|
<UButton v-bind="userStore.user" :label="userStore.user?.email"
|
||||||
|
trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost" square
|
||||||
|
class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||||
|
trailingIcon: 'text-dimmed ms-auto'
|
||||||
|
}" />
|
||||||
|
</UDropdownMenu>
|
||||||
|
<!-- first_name: '', last_name: '' -->
|
||||||
|
</template>
|
||||||
|
</USidebar>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||||
|
@click="open = !open" />
|
||||||
|
<p class="font-bold text-2xl">Customer-Management: <span class="text-[24px] font-bold">{{ pageTitle }}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex items-center gap-12">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<CountryCurrencySwitch />
|
||||||
|
<LangSwitch />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeSwitch />
|
||||||
|
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||||
|
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
||||||
|
{{ $t('general.logout') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useColorMode } from '@vueuse/core'
|
||||||
|
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
|
||||||
|
import { useAuthStore } from '../stores/customer/auth'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
await userStore.getUser()
|
||||||
|
|
||||||
|
const open = ref(true)
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const teams = ref([
|
||||||
|
{
|
||||||
|
label: 'Nuxt',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/nuxt.png',
|
||||||
|
alt: 'Nuxt'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Vue',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/vuejs.png',
|
||||||
|
alt: 'Vue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UnJS',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/unjs.png',
|
||||||
|
alt: 'UnJS'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const selectedTeam = ref(teams.value[0])
|
||||||
|
|
||||||
|
const teamsItems = computed<DropdownMenuItem[][]>(() => {
|
||||||
|
return [
|
||||||
|
teams.value.map((team, index) => ({
|
||||||
|
...team,
|
||||||
|
kbds: ['meta', String(index + 1)],
|
||||||
|
onSelect() {
|
||||||
|
selectedTeam.value = team
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Create team',
|
||||||
|
icon: 'i-lucide-circle-plus'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
function getItems(state: 'collapsed' | 'expanded') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Inbox',
|
||||||
|
icon: 'i-lucide-inbox',
|
||||||
|
badge: '4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Issues',
|
||||||
|
icon: 'i-lucide-square-dot'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Activity',
|
||||||
|
icon: 'i-lucide-square-activity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
icon: 'i-lucide-settings',
|
||||||
|
defaultOpen: true,
|
||||||
|
children:
|
||||||
|
state === 'expanded'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'General',
|
||||||
|
icon: 'i-lucide-house'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Team',
|
||||||
|
icon: 'i-lucide-users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Billing',
|
||||||
|
icon: 'i-lucide-credit-card'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
] satisfies NavigationMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { currentLang } from '@/router/langs'
|
||||||
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
|
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
|
||||||
|
import LangSwitch from '@/components/inner/LangSwitch.vue'
|
||||||
|
import ThemeSwitch from '@/components/inner/ThemeSwitch.vue'
|
||||||
|
import type { LabelTrans, TopMenuItem } from '@/types'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { components } from 'reka-ui/constant'
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const menu = ref<TopMenuItem[] | null>(null)
|
||||||
|
|
||||||
|
async function getTopMenu() {
|
||||||
|
try {
|
||||||
|
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
|
||||||
|
|
||||||
|
menu.value = items
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getTopMenu()
|
||||||
|
})
|
||||||
|
|
||||||
|
const menuItems = computed(() => {
|
||||||
|
if (!menu.value?.length) return []
|
||||||
|
|
||||||
|
return transformMenu(
|
||||||
|
menu.value || [],
|
||||||
|
currentLang.value?.iso_code
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function transformMenu(
|
||||||
|
items: TopMenuItem[],
|
||||||
|
locale: string | undefined
|
||||||
|
): NavigationMenuItem[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
const route: NavigationMenuItem = {
|
||||||
|
icon: item.label.icon || 'i-lucide-house',
|
||||||
|
label:
|
||||||
|
item.label.trans?.[locale as keyof LabelTrans]?.label ||
|
||||||
|
item.label.trans?.en?.label ||
|
||||||
|
'—',
|
||||||
|
children: item.children
|
||||||
|
? transformMenu(item.children, locale)
|
||||||
|
: undefined,
|
||||||
|
onSelect: () => {
|
||||||
|
router.push({
|
||||||
|
name: item.params.route.name,
|
||||||
|
params: {
|
||||||
|
...(item.params.route.params || {}),
|
||||||
|
locale: currentLang.value?.iso_code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const userItems = computed<DropdownMenuItem[][]>(() => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Profile',
|
||||||
|
icon: 'i-lucide-user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Billing',
|
||||||
|
icon: 'i-lucide-credit-card'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
icon: 'i-lucide-settings',
|
||||||
|
to: '/settings'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Appearance',
|
||||||
|
icon: 'i-lucide-sun-moon',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'Light',
|
||||||
|
icon: 'i-lucide-sun',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: colorMode.value === 'light',
|
||||||
|
onUpdateChecked(checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
colorMode.preference = 'light'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dark',
|
||||||
|
icon: 'i-lucide-moon',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: colorMode.value === 'dark',
|
||||||
|
onUpdateChecked(checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
colorMode.preference = 'dark'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
icon: 'i-simple-icons-github',
|
||||||
|
to: 'https://github.com/nuxt/ui',
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log out',
|
||||||
|
icon: 'i-lucide-log-out'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
defineShortcuts(extractShortcuts(teamsItems.value))
|
||||||
|
</script>
|
||||||
8
bo/src/types/user.d.ts
vendored
8
bo/src/types/user.d.ts
vendored
@@ -13,3 +13,11 @@ export interface User {
|
|||||||
nip?: string
|
nip?: string
|
||||||
vat?: string
|
vat?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
user_id: number
|
||||||
|
email: string
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user