chore: first setup ok
parent
4840b31c96
commit
af7e775216
|
|
@ -509,6 +509,7 @@ declare module 'vue' {
|
|||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']>
|
||||
readonly useAbility: UnwrapRef<typeof import('./src/plugins/casl/composables/useAbility')['useAbility']>
|
||||
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
|
|
@ -852,6 +853,7 @@ declare module '@vue/runtime-core' {
|
|||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']>
|
||||
readonly useAbility: UnwrapRef<typeof import('./src/plugins/casl/composables/useAbility')['useAbility']>
|
||||
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vuexy - Vuejs Admin Dashboard Template</title>
|
||||
<title>HelpDesk Web POS</title>
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,26 +12,11 @@
|
|||
class="mx-1"
|
||||
/>
|
||||
By <a
|
||||
href="https://pixinvent.com"
|
||||
href="https://inetum.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary ms-1"
|
||||
>Pixinvent</a>
|
||||
</span>
|
||||
<!-- 👉 Footer: right content -->
|
||||
<span class="d-md-flex gap-x-4 text-primary d-none">
|
||||
<a
|
||||
href="https://themeforest.net/licenses/standard"
|
||||
target="noopener noreferrer"
|
||||
>License</a>
|
||||
<a
|
||||
href="https://1.envato.market/pixinvent_portfolio"
|
||||
target="noopener noreferrer"
|
||||
>More Themes</a>
|
||||
<a
|
||||
href="https://demos.pixinvent.com/vuexy-vuejs-admin-template/documentation/"
|
||||
target="noopener noreferrer"
|
||||
>Documentation</a>
|
||||
>Inetum</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
|
||||
import { userStore } from '@stores/user.store'
|
||||
|
||||
const useUserStore = userStore()
|
||||
const router = useRouter()
|
||||
|
||||
const logoutAndRedirect = () => {
|
||||
useUserStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -48,9 +58,9 @@ import avatar1 from '@images/avatars/avatar-1.png'
|
|||
</template>
|
||||
|
||||
<VListItemTitle class="font-weight-semibold">
|
||||
John Doe
|
||||
{{ useUserStore.username }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>Admin</VListItemSubtitle>
|
||||
<VListItemSubtitle>{{ useUserStore.role }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
|
||||
<VDivider class="my-2" />
|
||||
|
|
@ -111,7 +121,7 @@ import avatar1 from '@images/avatars/avatar-1.png'
|
|||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Logout -->
|
||||
<VListItem to="/login">
|
||||
<VListItem @click="logoutAndRedirect">
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
class="me-2"
|
||||
|
|
|
|||
|
|
@ -9,4 +9,9 @@ export default [
|
|||
to: { name: 'second-page' },
|
||||
icon: { icon: 'tabler-file' },
|
||||
},
|
||||
{
|
||||
title: 'Store',
|
||||
to: { name: 'store' },
|
||||
icon: { icon: 'tabler-file' },
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,4 +9,9 @@ export default [
|
|||
to: { name: 'second-page' },
|
||||
icon: { icon: 'tabler-file' },
|
||||
},
|
||||
{
|
||||
title: 'Store',
|
||||
to: { name: 'store' },
|
||||
icon: { icon: 'tabler-file' },
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!-- ❗Errors in the form are set on line 60 -->
|
||||
<script setup lang="ts">
|
||||
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
|
||||
import authV2LoginIllustrationBorderedDark from '@images/pages/auth-v2-login-illustration-bordered-dark.png'
|
||||
import authV2LoginIllustrationBorderedLight from '@images/pages/auth-v2-login-illustration-bordered-light.png'
|
||||
|
|
@ -8,30 +9,62 @@ import authV2LoginIllustrationLight from '@images/pages/auth-v2-login-illustrati
|
|||
import authV2MaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import authV2MaskLight from '@images/pages/misc-mask-light.png'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { userStore } from '@stores/user.store'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
|
||||
const authThemeImg = useGenerateImageVariant(authV2LoginIllustrationLight, authV2LoginIllustrationDark, authV2LoginIllustrationBorderedLight, authV2LoginIllustrationBorderedDark, true)
|
||||
|
||||
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
layout: 'blank',
|
||||
unauthenticatedOnly: true,
|
||||
},
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
|
||||
const isPasswordVisible = ref(false)
|
||||
|
||||
const authThemeImg = useGenerateImageVariant(
|
||||
authV2LoginIllustrationLight,
|
||||
authV2LoginIllustrationDark,
|
||||
authV2LoginIllustrationBorderedLight,
|
||||
authV2LoginIllustrationBorderedDark,
|
||||
true)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
||||
const ability = useAbility()
|
||||
|
||||
const errors = ref<Record<string, string | undefined>>({
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
})
|
||||
|
||||
const refVForm = ref<VForm>()
|
||||
|
||||
const credentials = ref({
|
||||
username: 'Fred',
|
||||
password: 'admin123',
|
||||
})
|
||||
|
||||
const rememberMe = ref(false)
|
||||
const isSnackbarVisibility = ref(false)
|
||||
|
||||
const useUserStore = userStore()
|
||||
|
||||
const login = async () => {
|
||||
// update the userStore with the user data
|
||||
useUserStore.login(credentials.value.username, credentials.value.password, ability)
|
||||
|
||||
// Redirect to `to` query if exist or redirect to index route
|
||||
// ❗ nextTick is required to wait for DOM updates and later redirect
|
||||
await nextTick(() => {
|
||||
router.replace(route.query.to ? String(route.query.to) : '/')
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
refVForm.value?.validate()
|
||||
.then(({ valid: isValid }) => {
|
||||
if (isValid)
|
||||
login()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -40,8 +73,8 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
|||
class="auth-wrapper bg-surface"
|
||||
>
|
||||
<VCol
|
||||
md="8"
|
||||
class="d-none d-md-flex"
|
||||
lg="8"
|
||||
class="d-none d-lg-flex"
|
||||
>
|
||||
<div class="position-relative bg-background rounded-lg w-100 ma-8 me-0">
|
||||
<div class="d-flex align-center justify-center w-100 h-100">
|
||||
|
|
@ -53,15 +86,15 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
|||
</div>
|
||||
|
||||
<VImg
|
||||
class="auth-footer-mask"
|
||||
:src="authThemeMask"
|
||||
class="auth-footer-mask"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="auth-card-v2 d-flex align-center justify-center"
|
||||
>
|
||||
<VCard
|
||||
|
|
@ -74,49 +107,64 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
|||
:nodes="themeConfig.app.logo"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<h4 class="text-h4 mb-1">
|
||||
Welcome to <span class="text-capitalize">{{ themeConfig.app.title }}</span>! 👋🏻
|
||||
Welcome to <span class="text-capitalize"> {{ themeConfig.app.title }} </span>! 👋🏻
|
||||
</h4>
|
||||
<p class="mb-0">
|
||||
Please sign-in to your account and start the adventure
|
||||
</p>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => { }">
|
||||
<VAlert
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<p class="text-sm mb-2">
|
||||
Admin username: <strong>admin</strong> / Pass: <strong>admin</strong>
|
||||
</p>
|
||||
<p class="text-sm mb-0">
|
||||
Support username: <strong>support</strong> / Pass: <strong>support</strong>
|
||||
</p>
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<!-- email -->
|
||||
<!-- username -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.email"
|
||||
v-model="credentials.username"
|
||||
label="Username"
|
||||
placeholder="johndoe"
|
||||
type="username"
|
||||
autofocus
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="johndoe@email.com"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.username"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- password -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.password"
|
||||
v-model="credentials.password"
|
||||
label="Password"
|
||||
placeholder="············"
|
||||
:rules="[requiredValidator]"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:error-messages="errors.password"
|
||||
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center flex-wrap justify-space-between mt-2 mb-4">
|
||||
<div class="d-flex align-center flex-wrap justify-space-between mt-1 mb-4">
|
||||
<VCheckbox
|
||||
v-model="form.remember"
|
||||
v-model="rememberMe"
|
||||
label="Remember me"
|
||||
/>
|
||||
<a
|
||||
class="text-primary ms-2 mb-1"
|
||||
href="#"
|
||||
>
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
|
|
@ -126,46 +174,28 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
|
|||
Login
|
||||
</VBtn>
|
||||
</VCol>
|
||||
|
||||
<!-- create account -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center text-base"
|
||||
>
|
||||
<span>New on our platform?</span>
|
||||
|
||||
<a
|
||||
class="text-primary ms-2"
|
||||
href="#"
|
||||
>
|
||||
Create an account
|
||||
</a>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<VDivider />
|
||||
|
||||
<span class="mx-4">or</span>
|
||||
|
||||
<VDivider />
|
||||
</VCol>
|
||||
|
||||
<!-- auth providers -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<AuthProvider />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- Snackbar -->
|
||||
<VSnackbar
|
||||
v-model="isSnackbarVisibility"
|
||||
location="center"
|
||||
>
|
||||
Incorrect username or password ...
|
||||
|
||||
<template #actions>
|
||||
<VBtn
|
||||
color="error"
|
||||
@click="isSnackbarVisibility = false"
|
||||
>
|
||||
Close
|
||||
</VBtn>
|
||||
</template>
|
||||
</VSnackbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,418 @@
|
|||
<script setup lang="ts">
|
||||
import { VDataTableServer } from 'vuetify/labs/VDataTable'
|
||||
import { paginationMeta } from '@api-utils/paginationMeta'
|
||||
|
||||
const widgetData = ref([
|
||||
{ title: 'In-Store Sales', value: '$5,345.43', icon: 'tabler-home', desc: '5k orders', change: 5.7 },
|
||||
{ title: 'Website Sales', value: '$674,347.12', icon: 'tabler-device-laptop', desc: '21k orders', change: 12.4 },
|
||||
{ title: 'Discount', value: '$14,235.12', icon: 'tabler-gift', desc: '6k orders' },
|
||||
{ title: 'Affiliate', value: '$8,345.23', icon: 'tabler-wallet', desc: '150 orders', change: -3.5 },
|
||||
])
|
||||
|
||||
const headers = [
|
||||
{ title: 'Product', key: 'product' },
|
||||
{ title: 'Category', key: 'category' },
|
||||
{ title: 'Stock', key: 'stock', sortable: false },
|
||||
{ title: 'SKU', key: 'sku' },
|
||||
{ title: 'Price', key: 'price' },
|
||||
{ title: 'QTY', key: 'qty' },
|
||||
{ title: 'Status', key: 'status' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const selectedStatus = ref()
|
||||
const selectedCategory = ref()
|
||||
const selectedStock = ref<boolean | undefined>()
|
||||
const searchQuery = ref('')
|
||||
|
||||
const status = ref([
|
||||
{ title: 'Scheduled', value: 'Scheduled' },
|
||||
{ title: 'Publish', value: 'Published' },
|
||||
{ title: 'Inactive', value: 'Inactive' },
|
||||
])
|
||||
|
||||
const categories = ref([
|
||||
{ title: 'Accessories', value: 'Accessories' },
|
||||
{ title: 'Home Decor', value: 'Home Decor' },
|
||||
{ title: 'Electronics', value: 'Electronics' },
|
||||
{ title: 'Shoes', value: 'Shoes' },
|
||||
{ title: 'Office', value: 'Office' },
|
||||
{ title: 'Games', value: 'Games' },
|
||||
])
|
||||
|
||||
const stockStatus = ref([
|
||||
{ title: 'In Stock', value: true },
|
||||
{ title: 'Out of Stock', value: false },
|
||||
])
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
// Update data table options
|
||||
const updateOptions = (options: any) => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const resolveCategory = (category: string) => {
|
||||
if (category === 'Accessories')
|
||||
return { color: 'error', icon: 'tabler-device-watch' }
|
||||
if (category === 'Home Decor')
|
||||
return { color: 'info', icon: 'tabler-home' }
|
||||
if (category === 'Electronics')
|
||||
return { color: 'primary', icon: 'tabler-device-imac' }
|
||||
if (category === 'Shoes')
|
||||
return { color: 'success', icon: 'tabler-shoe' }
|
||||
if (category === 'Office')
|
||||
return { color: 'warning', icon: 'tabler-briefcase' }
|
||||
if (category === 'Games')
|
||||
return { color: 'primary', icon: 'tabler-device-gamepad-2' }
|
||||
}
|
||||
|
||||
const resolveStatus = (statusMsg: string) => {
|
||||
if (statusMsg === 'Scheduled')
|
||||
return { text: 'Scheduled', color: 'warning' }
|
||||
if (statusMsg === 'Published')
|
||||
return { text: 'Publish', color: 'success' }
|
||||
if (statusMsg === 'Inactive')
|
||||
return { text: 'Inactive', color: 'error' }
|
||||
}
|
||||
|
||||
const { data: productsData, execute: fetchProducts } = await useApi<any>(createUrl('/apps/ecommerce/products',
|
||||
{
|
||||
query: {
|
||||
q: searchQuery,
|
||||
stock: selectedStock,
|
||||
category: selectedCategory,
|
||||
status: selectedStatus,
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
const products = computed(() => productsData.value.products)
|
||||
const totalProduct = computed(() => productsData.value.total)
|
||||
|
||||
const deleteProduct = async (id: number) => {
|
||||
await $api(`apps/ecommerce/products/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
fetchProducts()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard title="Create Awesome 🙌">
|
||||
<VCardText>This is your second page.</VCardText>
|
||||
<!-- 👉 widgets -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
Chocolate sesame snaps pie carrot cake pastry pie lollipop muffin.
|
||||
Carrot cake dragée chupa chups jujubes. Macaroon liquorice cookie
|
||||
wafer tart marzipan bonbon. Gingerbread jelly-o dragée
|
||||
chocolate.
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs
|
||||
? 'product-widget'
|
||||
: $vuetify.display.sm
|
||||
? id < 2 ? 'product-widget' : ''
|
||||
: ''"
|
||||
>
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<div class="text-body-1 font-weight-medium text-capitalize">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
|
||||
<h4 class="text-h4 my-1">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="me-2 text-disabled text-no-wrap">
|
||||
{{ data.desc }}
|
||||
</div>
|
||||
|
||||
<VChip
|
||||
v-if="data.change"
|
||||
label
|
||||
:color="data.change > 0 ? 'success' : 'error'"
|
||||
>
|
||||
{{ prefixWithPlus(data.change) }}%
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="38"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="28"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
|
||||
: $vuetify.display.smAndUp ? id % 2 === 0
|
||||
: false"
|
||||
vertical
|
||||
inset
|
||||
length="95"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 products -->
|
||||
<VCard
|
||||
title="Filters"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 👉 Select Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Status"
|
||||
:items="status"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Category -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedCategory"
|
||||
placeholder="Category"
|
||||
:items="categories"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Stock Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedStock"
|
||||
placeholder="Stock"
|
||||
:items="stockStatus"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<div class="d-flex flex-wrap gap-4 mx-5">
|
||||
<div class="d-flex align-center">
|
||||
<!-- 👉 Search -->
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Product"
|
||||
density="compact"
|
||||
style="inline-size: 200px;"
|
||||
class="me-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
<div class="d-flex gap-4 flex-wrap align-center">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[5, 10, 20, 25, 50]"
|
||||
/>
|
||||
<!-- 👉 Export button -->
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="tabler-upload"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="$router.push('/apps/ecommerce/product/add')"
|
||||
>
|
||||
Add Product
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-4" />
|
||||
|
||||
<!-- 👉 Datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
show-select
|
||||
:items="products"
|
||||
:items-length="totalProduct"
|
||||
class="text-no-wrap"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- product -->
|
||||
<template #item.product="{ item }">
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<VAvatar
|
||||
v-if="item.image"
|
||||
size="38"
|
||||
variant="tonal"
|
||||
rounded
|
||||
:image="item.image"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-body-1 font-weight-medium">{{ item.productName }}</span>
|
||||
<span class="text-sm text-disabled">{{ item.productBrand }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- category -->
|
||||
<template #item.category="{ item }">
|
||||
<VAvatar
|
||||
size="30"
|
||||
variant="tonal"
|
||||
:color="resolveCategory(item.category)?.color"
|
||||
class="me-2"
|
||||
>
|
||||
<VIcon
|
||||
:icon="resolveCategory(item.category)?.icon"
|
||||
size="18"
|
||||
/>
|
||||
</VAvatar>
|
||||
<span class="text-body-1 font-weight-medium">{{ item.category }}</span>
|
||||
</template>
|
||||
|
||||
<!-- stock -->
|
||||
<template #item.stock="{ item }">
|
||||
<VSwitch :model-value="item.stock" />
|
||||
</template>
|
||||
|
||||
<!-- status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
density="default"
|
||||
label
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-edit" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="download"
|
||||
prepend-icon="tabler-download"
|
||||
>
|
||||
Download
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="delete"
|
||||
prepend-icon="tabler-trash"
|
||||
@click="deleteProduct(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="duplicate"
|
||||
prepend-icon="tabler-copy"
|
||||
>
|
||||
Duplicate
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex align-center justify-space-between flex-wrap gap-3 pa-5 pt-3">
|
||||
<p class="text-sm text-medium-emphasis mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalProduct) }}
|
||||
</p>
|
||||
|
||||
<VPagination
|
||||
v-model="page"
|
||||
:length="Math.min(Math.ceil(totalProduct / itemsPerPage), 5)"
|
||||
:total-visible="$vuetify.display.xs ? 1 : Math.min(Math.ceil(totalProduct / itemsPerPage), 5)"
|
||||
>
|
||||
<template #prev="slotProps">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="default"
|
||||
v-bind="slotProps"
|
||||
:icon="false"
|
||||
>
|
||||
Previous
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<template #next="slotProps">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="default"
|
||||
v-bind="slotProps"
|
||||
:icon="false"
|
||||
>
|
||||
Next
|
||||
</VBtn>
|
||||
</template>
|
||||
</VPagination>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-widget{
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<VCard title="Stores 🙌">
|
||||
<VCardText>This is the Store Page.</VCardText>
|
||||
<VCardText>
|
||||
Liste des boutiques
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import type { Router } from 'vue-router'
|
||||
import { canNavigate } from '@layouts/plugins/casl'
|
||||
|
||||
export const setupGuards = (router: Router) => {
|
||||
// 👉 router.beforeEach
|
||||
// Docs: https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards
|
||||
router.beforeEach(to => {
|
||||
/*
|
||||
* If it's a public route, continue navigation. This kind of pages are allowed to visited by login & non-login users. Basically, without any restrictions.
|
||||
* Examples of public routes are, 404, under maintenance, etc.
|
||||
*/
|
||||
if (to.meta.public)
|
||||
return
|
||||
|
||||
/**
|
||||
* Check if user is logged in by checking if token & user data exists in local storage
|
||||
* Feel free to update this logic to suit your needs
|
||||
*/
|
||||
const isLoggedIn = !!(useCookie('userData').value && useCookie('accessToken').value)
|
||||
|
||||
/*
|
||||
If user is logged in and is trying to access login like page, redirect to home
|
||||
else allow visiting the page
|
||||
(WARN: Don't allow executing further by return statement because next code will check for permissions)
|
||||
*/
|
||||
if (to.meta.unauthenticatedOnly) {
|
||||
if (isLoggedIn)
|
||||
return '/'
|
||||
else
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!canNavigate(to)) {
|
||||
/* eslint-disable indent */
|
||||
return isLoggedIn
|
||||
? { name: 'not-authorized' }
|
||||
: {
|
||||
name: 'login',
|
||||
query: {
|
||||
...to.query,
|
||||
to: to.fullPath !== '/' ? to.path : undefined,
|
||||
},
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { createMongoAbility } from '@casl/ability'
|
||||
|
||||
export type Actions = 'create' | 'read' | 'update' | 'delete' | 'manage'
|
||||
|
||||
// ex: Post, Comment, User, etc. We haven't used any of these in our demo though.
|
||||
export type Subjects = 'Post' | 'Comment' | 'all'
|
||||
|
||||
export interface Rule { action: Actions; subject: Subjects }
|
||||
|
||||
export const ability = createMongoAbility<[Actions, Subjects]>()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { useAbility as useCaslAbility } from '@casl/vue'
|
||||
import type { ability } from '../ability'
|
||||
|
||||
export const useAbility = () => useCaslAbility<typeof ability>()
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { App } from 'vue'
|
||||
|
||||
import { createMongoAbility } from '@casl/ability'
|
||||
import { abilitiesPlugin } from '@casl/vue'
|
||||
import type { Rule } from './ability'
|
||||
|
||||
export default function (app: App) {
|
||||
const userAbilityRules = useCookie<Rule[]>('userAbilityRules')
|
||||
const initialAbility = createMongoAbility(userAbilityRules.value ?? [])
|
||||
|
||||
app.use(abilitiesPlugin, initialAbility, {
|
||||
useGlobalProperties: true,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { ability } from './ability';
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$ability: typeof ability;
|
||||
$can(this: this, ...args: Parameters<this['$ability']['can']>): boolean;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export const genId = <T extends { id: number | string }>(array: T[]) => {
|
||||
const { length } = array
|
||||
|
||||
let lastIndex = 0
|
||||
|
||||
if (length)
|
||||
lastIndex = Number(array[length - 1]?.id) + 1
|
||||
|
||||
return lastIndex || (length + 1)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const paginateArray = (array: unknown[], perPage: number, page: number) => array.slice((page - 1) * perPage, page * perPage)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const paginationMeta = <T extends { page: number; itemsPerPage: number }>(options: T, total: number) => {
|
||||
const start = (options.page - 1) * options.itemsPerPage + 1
|
||||
const end = Math.min(options.page * options.itemsPerPage, total)
|
||||
|
||||
return `Showing ${total === 0 ? 0 : start} to ${end} of ${total} entries`
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
interface UserState {
|
||||
username: string | null
|
||||
role: string | null
|
||||
}
|
||||
|
||||
export const userStore = defineStore('user', {
|
||||
state: (): UserState => ({
|
||||
username: null,
|
||||
role: null,
|
||||
}),
|
||||
actions: {
|
||||
async login(username: string, password: string, ability: any) {
|
||||
try {
|
||||
const res = await $api('http://localhost:8080/hdpos/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
|
||||
const { accessToken, userData, userAbilityRules: { rules } } = res
|
||||
|
||||
this.username = username
|
||||
this.role = password === 'admin123' ? 'Admin' : 'Support'
|
||||
|
||||
useCookie('accessToken').value = accessToken
|
||||
useCookie('userData').value = userData
|
||||
useCookie('userAbilityRules').value = rules
|
||||
ability.update(rules)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.username = null
|
||||
this.role = null
|
||||
|
||||
// Effacer les cookies
|
||||
useCookie('accessToken').value = null
|
||||
useCookie('userData').value = null
|
||||
useCookie('userAbilityRules').value = null
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -7,7 +7,8 @@ export const $api = ofetch.create({
|
|||
if (accessToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
const { global } = useTheme()
|
||||
|
||||
const authProviders = [
|
||||
{
|
||||
icon: 'fa-facebook',
|
||||
color: '#4267b2',
|
||||
colorInDark: '#4267b2',
|
||||
},
|
||||
{
|
||||
icon: 'fa-google',
|
||||
color: '#dd4b39',
|
||||
colorInDark: '#db4437',
|
||||
},
|
||||
{
|
||||
icon: 'fa-twitter',
|
||||
color: '#1da1f2',
|
||||
colorInDark: '#1da1f2',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex justify-center flex-wrap gap-3">
|
||||
<VBtn
|
||||
v-for="link in authProviders"
|
||||
:key="link.icon"
|
||||
icon
|
||||
variant="tonal"
|
||||
size="38"
|
||||
:color="global.name.value === 'dark' ? link.colorInDark : link.color"
|
||||
class="rounded"
|
||||
>
|
||||
<VIcon
|
||||
size="18"
|
||||
:icon="link.icon"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -10,10 +10,10 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
|
|||
|
||||
export const { themeConfig, layoutConfig } = defineThemeConfig({
|
||||
app: {
|
||||
title: 'vuexy',
|
||||
title: 'hdpos',
|
||||
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
|
||||
contentWidth: ContentWidth.Boxed,
|
||||
contentLayoutNav: AppContentLayoutNav.Vertical,
|
||||
contentLayoutNav: AppContentLayoutNav.Horizontal,
|
||||
overlayNavFromBreakpoint: breakpointsVuetify.md + 16, // 16 for scrollbar. Docs: https://next.vuetifyjs.com/en/features/display-and-platform/
|
||||
i18n: {
|
||||
enable: false,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@
|
|||
],
|
||||
"@api-utils/*": [
|
||||
"./src/plugins/fake-api/utils/*"
|
||||
],
|
||||
"@stores/*": [
|
||||
"./src/stores/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ declare module 'vue-router/auto/routes' {
|
|||
'$error': RouteRecordInfo<'$error', '/:error(.*)', { error: ParamValue<true> }, { error: ParamValue<false> }>,
|
||||
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
|
||||
'second-page': RouteRecordInfo<'second-page', '/second-page', Record<never, never>, Record<never, never>>,
|
||||
'store': RouteRecordInfo<'store', '/store', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export default defineConfig({
|
|||
'apexcharts': fileURLToPath(new URL('node_modules/apexcharts-clevision', import.meta.url)),
|
||||
'@db': fileURLToPath(new URL('./src/plugins/fake-api/handlers/', import.meta.url)),
|
||||
'@api-utils': fileURLToPath(new URL('./src/plugins/fake-api/utils/', import.meta.url)),
|
||||
'@stores': fileURLToPath(new URL('./src/stores/', import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue