Merge remote-tracking branch 'origin/translate' into front-styles
This commit is contained in:
11
bo/components.d.ts
vendored
11
bo/components.d.ts
vendored
@@ -11,12 +11,16 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ButtonGoToProfile: typeof import('./src/components/customer-management/ButtonGoToProfile.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']
|
||||
copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
||||
CountryCurrencySwitch: typeof import('./src/components/inner/CountryCurrencySwitch.vue')['default']
|
||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
|
||||
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
|
||||
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
|
||||
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
|
||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||
@@ -33,8 +37,10 @@ declare module 'vue' {
|
||||
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
||||
ProductEditor: typeof import('./src/components/inner/ProductEditor.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']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
|
||||
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
|
||||
TopBar: typeof import('./src/components/TopBar.vue')['default']
|
||||
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
|
||||
@@ -57,9 +63,12 @@ declare module 'vue' {
|
||||
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.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']
|
||||
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']
|
||||
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']
|
||||
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||
UTree: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tree.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { useAuthStore } from './stores/customer/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,35 +15,3 @@ import { RouterView } from 'vue-router'
|
||||
</template>
|
||||
|
||||
|
||||
<!-- <template>
|
||||
<component :is="layoutComponent">
|
||||
<Suspense>
|
||||
<RouterView />
|
||||
</Suspense>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import DefaultLayout from '@/layouts/default.vue'
|
||||
import EmptyLayout from '@/layouts/empty.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const layouts = {
|
||||
default: DefaultLayout,
|
||||
auth: EmptyLayout
|
||||
}
|
||||
|
||||
console.log(route.fullPath)
|
||||
console.log(route.name)
|
||||
console.log(route.matched)
|
||||
|
||||
const layoutComponent = computed(() => {
|
||||
console.log(route.meta);
|
||||
|
||||
return layouts[route.meta.layout as keyof typeof layouts] || DefaultLayout
|
||||
})
|
||||
</script> -->
|
||||
|
||||
@@ -47,7 +47,7 @@ export const uiOptions: NuxtUIOptions = {
|
||||
table: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||
// tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import LangSwitch from './inner/LangSwitch.vue'
|
||||
import ThemeSwitch from './inner/ThemeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { computed, ref } from 'vue'
|
||||
import { currentLang } from '@/router/langs'
|
||||
import type { LabelTrans, TopMenuItem } from '@/types'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import LangSwitch from './inner/LangSwitch.vue'
|
||||
import ThemeSwitch from './inner/ThemeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
12
bo/src/components/admin/FavoriteProducts.vue
Normal file
12
bo/src/components/admin/FavoriteProducts.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Default from '@/layouts/default.vue';
|
||||
|
||||
</script>
|
||||
@@ -141,7 +141,7 @@ async function fetchProductList() {
|
||||
if (route.params.category_id)
|
||||
params.append('category_id', String(route.params.category_id))
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}`
|
||||
const url = `/api/v1/restricted/product/list?elems=${perPage.value}&${params.toString()}`
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
@@ -161,7 +161,7 @@ function goToProduct(productId: number, linkRewrite: string) {
|
||||
}
|
||||
localStorage.setItem('back_from_product', JSON.stringify(path))
|
||||
router.push({
|
||||
name: 'customer-product-details',
|
||||
name: 'admin-product-details',
|
||||
params: { product_id: productId, link_rewrite: linkRewrite }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ import { useEditable } from '@/composable/useConteditable';
|
||||
import Default from '@/layouts/default.vue';
|
||||
import { langs } from '@/router/langs';
|
||||
import { useProductStore } from '@/stores/product';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useSettingsStore } from '@/stores/admin/settings';
|
||||
import type { EditorToolbarItem } from '@nuxt/ui';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -90,6 +90,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="" v-if="isTranslations">
|
||||
<p>Link rewrite:</p>
|
||||
<UTextarea :rows="1" v-model="productStore.productDescription.link_rewrite" autoresize :ui="{
|
||||
root: 'w-full',
|
||||
base: 'bg-inherit!',
|
||||
}" />
|
||||
</div>
|
||||
|
||||
<div class="" v-if="isTranslations">
|
||||
<p>Link rewrite:</p>
|
||||
<UTextarea :rows="1" v-model="productStore.productDescription.link_rewrite" autoresize :ui="{
|
||||
@@ -152,8 +160,8 @@
|
||||
<script setup lang="ts">
|
||||
import Default from '@/layouts/default.vue';
|
||||
import { langs } from '@/router/langs';
|
||||
import { useProductStore } from '@/stores/product';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useProductStore } from '@/stores/admin/product';
|
||||
import { useSettingsStore } from '@/stores/admin/settings';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import ProductEditor from '../inner/ProductEditor.vue';
|
||||
|
||||
211
bo/src/components/admin/UsersList.vue
Normal file
211
bo/src/components/admin/UsersList.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="flex flex-col md:flex-row gap-10">
|
||||
<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 { 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
|
||||
const 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 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(k => {
|
||||
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
|
||||
})
|
||||
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)
|
||||
|
||||
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 }) => 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 }) => 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 }) => 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'
|
||||
}, () => 'Manage')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.user_id
|
||||
return h(UButton, {
|
||||
color: 'info',
|
||||
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>
|
||||
245
bo/src/components/admin/UsersSearch.vue
Normal file
245
bo/src/components/admin/UsersSearch.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<component :is="Default">
|
||||
<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 Users</h1>
|
||||
|
||||
<div class="w-full max-w-4xl">
|
||||
<UInput icon="i-lucide-search" type="text" placeholder="Type user name or ID..." v-model="searchQuery"
|
||||
class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
|
||||
</div>
|
||||
|
||||
<p v-if="loading">Loading...</p>
|
||||
<p v-else-if="error" class="text-red-600">{{ error }}</p>
|
||||
<div v-else-if="clients.length" class="w-full max-w-4xl mt-7">
|
||||
<UTable :columns="columns" :data="clients" :ui="{ root: 'w-full!' }" />
|
||||
</div>
|
||||
|
||||
<p v-else-if="searchQuery.length" class="pt-4 text-gray-700">
|
||||
No users found with that name or ID
|
||||
</p>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, resolveComponent, h } from 'vue'
|
||||
import Default from '@/layouts/default.vue';
|
||||
import type { TableColumn } from '@nuxt/ui';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useFetchJson } from '@/composable/useFetchJson';
|
||||
|
||||
|
||||
interface Customer {
|
||||
user_id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
|
||||
const searchQuery = ref('');
|
||||
const clients = ref<Customer[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const limit = ref(10);
|
||||
const page = ref(1);
|
||||
|
||||
|
||||
function debounce(fn: Function, delay = 400) {
|
||||
let t: any;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const searchClients = async () => {
|
||||
if (!searchQuery.value) {
|
||||
clients.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await useFetchJson(
|
||||
`/api/v1/restricted/customer/list?search=${encodeURIComponent(searchQuery.value)}&elems=${limit.value}&page=${page.value}`
|
||||
);
|
||||
|
||||
clients.value = result.items?.items || [];
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch clients';
|
||||
clients.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
watch(searchQuery, debounce(searchClients, 300));
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
const UInput = resolveComponent('UInput')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
const UButton = resolveComponent('UButton')
|
||||
|
||||
|
||||
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
|
||||
const 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 })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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 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(k => {
|
||||
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
|
||||
})
|
||||
router.push({ query: { ...baseQuery, ...val, page: 1 } })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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 columns: TableColumn<Customer>[] = [
|
||||
{
|
||||
accessorKey: 'user_id',
|
||||
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 }) => 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 }) => 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'
|
||||
}, () => 'Manage')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.user_id
|
||||
return h(UButton, {
|
||||
color: 'info',
|
||||
size: 'sm',
|
||||
variant: 'soft',
|
||||
onClick: () => {
|
||||
router.push({
|
||||
name: 'customer-management-profile',
|
||||
params: { user_id: userId }
|
||||
})
|
||||
}
|
||||
}, () => 'Go to profile')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
</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 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useCartStore } from '@/stores/customer/cart'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useAddressStore } from '@/stores/address'
|
||||
import { useAddressStore } from '@/stores/customer/address'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Default from '@/layouts/default.vue'
|
||||
const addressStore = useAddressStore()
|
||||
|
||||
@@ -152,8 +152,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useAddressStore } from '@/stores/address'
|
||||
import { useCartStore } from '@/stores/customer/cart'
|
||||
import { useAddressStore } from '@/stores/customer/address'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Default from '@/layouts/default.vue'
|
||||
|
||||
@@ -3,15 +3,23 @@
|
||||
<div class="">
|
||||
<div class="flex md:flex-row flex-col justify-between gap-8 my-6">
|
||||
<div class="flex-1">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||
class="max-w-full h-auto object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ productData.name }}
|
||||
</h1>
|
||||
|
||||
<UIcon name="material-symbols:favorite"
|
||||
class="cursor-pointer text-2xl transition hover:scale-110"
|
||||
:class="productData.is_favorite ? 'text-red-500' : 'text-gray-400'"
|
||||
@click="toggleFavorite" />
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ productData.description }}
|
||||
</p>
|
||||
@@ -43,7 +51,8 @@
|
||||
</div>
|
||||
<div class="flex gap-5 items-end">
|
||||
<UInputNumber v-model="value" />
|
||||
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
||||
<UButton color="primary"
|
||||
class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
||||
Add to Cart
|
||||
</UButton>
|
||||
</div>
|
||||
@@ -78,6 +87,8 @@ import { ref, computed } from 'vue'
|
||||
import ProductCustomization from './components/ProductCustomization.vue'
|
||||
import ProductVariants from './components/ProductVariants.vue'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { useRoute } from 'vue-router'
|
||||
interface Color {
|
||||
id: string
|
||||
name: string
|
||||
@@ -97,6 +108,7 @@ interface ProductData {
|
||||
howToUseText: string
|
||||
productDetailsText: string
|
||||
documentsText: string
|
||||
is_favorite: boolean
|
||||
}
|
||||
|
||||
const activeTab = ref('description')
|
||||
@@ -157,6 +169,24 @@ const activeTabContent = computed(() => {
|
||||
if (productData.colors.length > 0) {
|
||||
selectedColor.value = productData.colors[0] as Color
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
async function toggleFavorite() {
|
||||
const url = `/api/v1/restricted/product/favorite/${route.params.product_id}`
|
||||
|
||||
try {
|
||||
if (!productData.is_favorite) {
|
||||
await useFetchJson(url, { method: 'POST' })
|
||||
} else {
|
||||
await useFetchJson(url, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
productData.is_favorite = !productData.is_favorite
|
||||
} catch (e: unknown) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,27 +11,30 @@
|
||||
</template>
|
||||
</UNavigationMenu> -->
|
||||
<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">
|
||||
<div v-if="customerProductStore.loading" class="text-center py-8">
|
||||
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
<div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ customerProductStore.error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="flex gap-2">
|
||||
<CategoryMenu />
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||
<UTable :data="customerProductStore.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" />
|
||||
<UPagination v-model:page="page" :total="customerProductStore.total"
|
||||
:page-size="customerProductStore.perPage" />
|
||||
</div>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div v-if="customerProductStore.productsList.length === 0"
|
||||
class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,20 +45,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 CategoryMenu from '../inner/CategoryMenu.vue'
|
||||
import { useCustomerProductStore } from '@/stores/customer/customer-product'
|
||||
import type { Product } from '@/stores/customer/customer-product'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
|
||||
const customerProductStore = useCustomerProductStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -101,18 +99,6 @@ const sortField = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = computed<Record<string, string>>({
|
||||
get: () => {
|
||||
const q = { ...route.query }
|
||||
@@ -157,36 +143,16 @@ const updateFilter = debounce((columnId: string, val: string) => {
|
||||
filters.value = newFilters
|
||||
}, 400)
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?${params}`
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
name: 'customer-product-details',
|
||||
params: { product_id: productId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const selectedCount = ref({
|
||||
product_id: null,
|
||||
count: 0
|
||||
@@ -205,7 +171,7 @@ const UInput = resolveComponent('UInput')
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
const columns: TableColumn<Product>[] = [
|
||||
{
|
||||
id: 'expand',
|
||||
cell: ({ row }) =>
|
||||
@@ -351,6 +317,35 @@ const columns: TableColumn<Payment>[] = [
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
class: 'cursor-pointer',
|
||||
color: 'info',
|
||||
variant: 'soft'
|
||||
}, () => 'Show product')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'counta',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UIcon, {
|
||||
onClick: () => customerProductStore.toggleFavorite(row.original),
|
||||
class: [
|
||||
'cursor-pointer text-[20px] transition-transform duration-200 hover:scale-125',
|
||||
row.original.is_favorite ? 'text-red-500' : 'text-blue-500'
|
||||
],
|
||||
name: 'material-symbols:favorite',
|
||||
variant: 'soft',
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -417,13 +412,27 @@ const columnsChild: TableColumn<Payment>[] = [
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
class: 'cursor-pointer',
|
||||
color: 'info',
|
||||
variant: 'soft'
|
||||
}, () => 'Show product')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
fetchProductList()
|
||||
customerProductStore.fetchProductList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCustomerStore } from '@/stores/customer'
|
||||
import { useAddressStore } from '@/stores/address'
|
||||
import { useAddressStore } from '@/stores/customer/address'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Default from '@/layouts/default.vue'
|
||||
const router = useRouter()
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCustomerStore } from '@/stores/customer'
|
||||
import { useAddressStore } from '@/stores/address'
|
||||
import { useAddressStore } from '@/stores/customer/address'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useCartStore } from '@/stores/customer/cart'
|
||||
import Default from '@/layouts/default.vue'
|
||||
const router = useRouter()
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
161
bo/src/components/customer/StorageFileBrowser.vue
Normal file
161
bo/src/components/customer/StorageFileBrowser.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="p-4">
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<ULoader />
|
||||
</div>
|
||||
<div v-else-if="error" class="text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
<UTree v-if="showTree" :items="treeItems" v-model:expanded="expandedFolders" :key="treeKey" @toggle="onToggle" :get-key="item => item.value">
|
||||
<template #item-wrapper="{ item }">
|
||||
<div class="flex items-start cursor-pointer" @click.stop="!item.isFolder">
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon :name="item.icon" :size="30" />
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-[15px] font-medium">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral"
|
||||
variant="outline" icon="i-lucide-download"
|
||||
@click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UTree>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
|
||||
interface FileItemRaw {
|
||||
Name: string
|
||||
IsFolder: boolean
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
name: string
|
||||
type: 'file' | 'folder'
|
||||
}
|
||||
|
||||
interface TreeItem {
|
||||
label: string
|
||||
icon: string
|
||||
children?: TreeItem[]
|
||||
isFolder: boolean
|
||||
path: string
|
||||
value: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{ initialPath?: string }>()
|
||||
|
||||
const currentPath = ref(props.initialPath || '')
|
||||
|
||||
const allData = ref<Record<string, FileItem[]>>({})
|
||||
const expandedFolders = ref<string[]>([])
|
||||
const treeKey = ref(0)
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const showTree = computed(() => !error.value)
|
||||
|
||||
async function fetchFolderContents(path: string): Promise<FileItem[]> {
|
||||
const url = `/api/v1/restricted/storage/list-content/${path}`
|
||||
const data = await useFetchJson<FileItemRaw[]>(url)
|
||||
|
||||
return (data.items || []).map(i => ({
|
||||
name: i.Name,
|
||||
type: i.IsFolder ? 'folder' : 'file'
|
||||
}))
|
||||
}
|
||||
|
||||
async function loadFolder(path: string) {
|
||||
if (allData.value[path]) return
|
||||
|
||||
try {
|
||||
const items = await fetchFolderContents(path)
|
||||
allData.value[path] = items
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load folder contents'
|
||||
}
|
||||
}
|
||||
|
||||
function buildTree(path: string): TreeItem[] {
|
||||
const items = allData.value[path] || []
|
||||
|
||||
return items.map(item => {
|
||||
const itemPath = path ? `${path}/${item.name}` : item.name
|
||||
const isFolder = item.type === 'folder'
|
||||
|
||||
const isExpanded = expandedFolders.value.includes(itemPath)
|
||||
const isLoaded = !!allData.value[itemPath]
|
||||
|
||||
return {
|
||||
label: item.name,
|
||||
icon: isFolder ? 'fxemoji:folder' : 'flat-color-icons:file',
|
||||
isFolder,
|
||||
path: isFolder ? itemPath : path,
|
||||
value: itemPath,
|
||||
fileName: isFolder ? undefined : item.name,
|
||||
|
||||
children: isFolder && isExpanded && isLoaded
|
||||
? buildTree(itemPath)
|
||||
: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const treeItems = computed(() => buildTree(currentPath.value))
|
||||
|
||||
async function toggleFolder(item: TreeItem) {
|
||||
if (!item.isFolder) return
|
||||
|
||||
const isOpen = expandedFolders.value.includes(item.value)
|
||||
|
||||
if (!isOpen) {
|
||||
await loadFolder(item.value)
|
||||
treeKey.value++
|
||||
} else {
|
||||
treeKey.value++
|
||||
}
|
||||
}
|
||||
|
||||
function onToggle(_event: unknown, item: TreeItem) {
|
||||
console.log('Toggle:', item)
|
||||
if (item.isFolder) {
|
||||
toggleFolder(item)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(path: string, fileName: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/restricted/storage/download-file/${path}/${fileName}`)
|
||||
if (!response.ok) throw new Error('Download failed')
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert('Download failed')
|
||||
}
|
||||
}
|
||||
|
||||
loadFolder(currentPath.value)
|
||||
</script>
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" />
|
||||
<UNavigationMenu orientation="vertical" type="single" :items="items" class="data-[orientation=vertical]:w-72" :ui="{
|
||||
root:''
|
||||
}"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -32,6 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useThemeStore } from '@/stores/admin/theme'
|
||||
const themeStorage = useThemeStore()
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function useFetchJson<T = unknown>(url: string, opt?: RequestInit):
|
||||
|
||||
// Handle 401 — access token expired; try to refresh via the HTTPOnly refresh_token cookie
|
||||
if (res.status === 401) {
|
||||
const { useAuthStore } = await import('@/stores/auth')
|
||||
const { useAuthStore } = await import('../stores/customer/auth')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const refreshed = await authStore.refreshAccessToken()
|
||||
|
||||
@@ -23,18 +23,23 @@
|
||||
<template #footer>
|
||||
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
||||
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||
<UButton v-bind="user" :label="user?.name" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
|
||||
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||
<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" />
|
||||
<span class="text-[20px] font-medium">{{ pageTitle }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-12">
|
||||
<div class="flex items-center gap-2">
|
||||
<CountryCurrencySwitch />
|
||||
@@ -43,14 +48,15 @@
|
||||
<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">
|
||||
class="flex gap-2 items-center 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') }}
|
||||
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500"/>
|
||||
</button>
|
||||
</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 />
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,10 +68,16 @@ 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 { LabelTrans, TopMenuItem } from '@/types'
|
||||
import { useAuthStore } from '../stores/customer/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const route = useRoute()
|
||||
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||
await userStore.getUser()
|
||||
|
||||
const open = ref(true)
|
||||
const authStore = useAuthStore()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const teams = ref([
|
||||
@@ -152,13 +164,14 @@ function getItems(state: 'collapsed' | 'expanded') {
|
||||
}
|
||||
|
||||
//
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { currentLang } from '@/router/langs'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
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'
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
@@ -217,13 +230,6 @@ function transformMenu(
|
||||
})
|
||||
}
|
||||
|
||||
const user = ref({
|
||||
name: 'Benjamin Canac',
|
||||
avatar: {
|
||||
src: 'https://github.com/benjamincanac.png',
|
||||
alt: 'Benjamin Canac'
|
||||
}
|
||||
})
|
||||
|
||||
const userItems = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
|
||||
311
bo/src/layouts/management.vue
Normal file
311
bo/src/layouts/management.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<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 }">
|
||||
<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-xl">Customer-Management: <span class="text-[20px] font-medium">{{ 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="flex gap-2 items-center 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') }}
|
||||
<UIcon name="ic:baseline-logout" class="text-red-700 dark:text-red-500" />
|
||||
</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()
|
||||
|
||||
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 { watch } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const menu = ref<TopMenuItem[] | null>(null)
|
||||
|
||||
const Id =Number(route.params.user_id)
|
||||
|
||||
async function cmGetTopMenu() {
|
||||
try {
|
||||
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
|
||||
|
||||
menu.value = items
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(route)
|
||||
watch(
|
||||
() => route.params.user_id,
|
||||
() => {
|
||||
if (route.params.user_id) {
|
||||
cmGetTopMenu()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { currentLang, langs, switchLocalization } from './langs'
|
||||
import { getSettings } from './settings'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { getRoutes } from './menu'
|
||||
|
||||
function isAuthenticated(): boolean {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Address } from './address'
|
||||
|
||||
export interface CustomerData {
|
||||
companyName: string
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { useUserStore } from '../user'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
@@ -41,6 +42,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
_isAuthenticated.value = readIsAuthenticatedCookie()
|
||||
}
|
||||
|
||||
// const auth = useAuthStore()
|
||||
// const userStore = useUserStore()
|
||||
async function login(email: string, password: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
@@ -60,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
user.value = response.user
|
||||
_syncAuthState()
|
||||
// await userStore.getUser()
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
86
bo/src/stores/customer/customer-product.ts
Normal file
86
bo/src/stores/customer/customer-product.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
|
||||
export interface Product {
|
||||
id: number
|
||||
image: string
|
||||
name: string
|
||||
productDetails?: string
|
||||
product_id: number
|
||||
is_favorite?: boolean
|
||||
}
|
||||
|
||||
export interface ProductResponse {
|
||||
items: Product[]
|
||||
items_count: number
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export const useCustomerProductStore = defineStore('customer-product', () => {
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const route = useRoute()
|
||||
const productsList = ref<Product[]>([])
|
||||
const total = ref(0)
|
||||
const perPage = ref(15)
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/product/list?${params}`
|
||||
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFavorite(product: Product) {
|
||||
const productId = product.product_id
|
||||
const isFavorite = product.is_favorite
|
||||
|
||||
const url = `/api/v1/restricted/product/favorite/${productId}`
|
||||
|
||||
try {
|
||||
if (!isFavorite) {
|
||||
await useFetchJson(url, { method: 'POST' })
|
||||
} else {
|
||||
await useFetchJson(url, { method: 'DELETE' })
|
||||
}
|
||||
product.is_favorite = !isFavorite
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to update favorite'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fetchProductList,
|
||||
toggleFavorite,
|
||||
productsList,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
perPage
|
||||
}
|
||||
})
|
||||
32
bo/src/stores/user.ts
Normal file
32
bo/src/stores/user.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { User } from '@/types/user'
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const error = ref<string | null>(null)
|
||||
const user = ref<User | null>(null)
|
||||
|
||||
async function getUser() {
|
||||
error.value = null
|
||||
try {
|
||||
const data = await useFetchJson<User>(`/api/v1/restricted/customer`)
|
||||
console.log('getUser API response:', data)
|
||||
|
||||
const response: User = (data as any).items ?? data
|
||||
console.log('User response:', response)
|
||||
user.value = response
|
||||
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err?.message ?? 'Unknown error'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
user,
|
||||
getUser
|
||||
}
|
||||
})
|
||||
23
bo/src/types/user.d.ts
vendored
Normal file
23
bo/src/types/user.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
company_name?: string
|
||||
company_email?: string
|
||||
company_address?: Address
|
||||
billing_address?: Address
|
||||
regon?: string
|
||||
nip?: string
|
||||
vat?: string
|
||||
}
|
||||
|
||||
|
||||
interface Customer {
|
||||
user_id: number
|
||||
email: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
<script setup lang="ts">
|
||||
// import { useRoute } from 'vue-router';
|
||||
import Default from '@/layouts/default.vue';
|
||||
import { useCategoryStore } from '@/stores/category';
|
||||
import { useCategoryStore } from '@/stores/admin/category';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
// const route = useRoute()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { useValidation } from '@/composable/useValidation'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { useValidation } from '@/composable/useValidation'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineAsyncComponent } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { useValidation } from '@/composable/useValidation'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
import { getYears, getQuarters, getIssues, type QuarterData, type IssueTimeSummary } from '@/composable/useRepoApi'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/customer/auth'
|
||||
import { useValidation } from '@/composable/useValidation'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
12
bo/src/views/StorageView.vue
Normal file
12
bo/src/views/StorageView.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<Default>
|
||||
<div class="h-full">
|
||||
<StorageFileBrowser initial-path="dest/src" />
|
||||
</div>
|
||||
</Default>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Default from '@/layouts/default.vue'
|
||||
import StorageFileBrowser from '@/components/customer/StorageFileBrowser.vue'
|
||||
</script>
|
||||
Reference in New Issue
Block a user