Merge pull request 'refactor/issue-1/first-setup' (#9) from refactor/issue-1/first-setup into dev

Reviewed-on: #9
pull/10/head
Frédérik Benoist 2024-01-02 21:20:23 +01:00
commit c2d60ae2b3
63 changed files with 4350 additions and 237 deletions

View File

@ -1 +0,0 @@
VITE_API_BASE_URL=

2
auto-imports.d.ts vendored
View File

@ -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']>

View File

@ -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>

View File

@ -31,6 +31,8 @@
"apexcharts-clevision": "^3.28.5",
"chart.js": "^4.4.0",
"cookie-es": "^1.0.0",
"date-fns": "^3.0.6",
"export-from-json": "^1.7.4",
"jwt-decode": "^3.1.2",
"mapbox-gl": "2.15.0",
"ofetch": "^1.3.3",
@ -133,4 +135,4 @@
"msw": {
"workerDirectory": "public"
}
}
}

View File

@ -67,6 +67,12 @@ dependencies:
cookie-es:
specifier: ^1.0.0
version: 1.0.0
date-fns:
specifier: ^3.0.6
version: 3.0.6
export-from-json:
specifier: ^1.7.4
version: 1.7.4
jwt-decode:
specifier: ^3.1.2
version: 3.1.2
@ -3198,6 +3204,10 @@ packages:
/dash-get@1.0.2:
resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
/date-fns@3.0.6:
resolution: {integrity: sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==}
dev: false
/de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true
@ -4109,6 +4119,10 @@ packages:
strip-final-newline: 3.0.0
dev: true
/export-from-json@1.7.4:
resolution: {integrity: sha512-FjmpluvZS2PTYyhkoMfQoyEJMfe2bfAyNpa5Apa6C9n7SWUWyJkG/VFnzERuj3q9Jjo3iwBjwVsDQ7Z7sczthA==}
dev: false
/external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}

View File

@ -1,8 +1,10 @@
import { useStorage } from '@vueuse/core'
import flatpickr from 'flatpickr'
import { French } from 'flatpickr/dist/l10n/fr'
import { useTheme } from 'vuetify'
import { useConfigStore } from '@core/stores/config'
import { cookieRef, namespaceConfig } from '@layouts/stores/config'
import { themeConfig } from '@themeConfig'
import { cookieRef, namespaceConfig } from '@layouts/stores/config'
import { useConfigStore } from '@core/stores/config'
const _syncAppRtl = () => {
const configStore = useConfigStore()
@ -14,6 +16,15 @@ const _syncAppRtl = () => {
if (locale.value !== storedLang.value && storedLang.value)
locale.value = storedLang.value
if (locale.value === 'fr') {
if (flatpickr.l10ns.fr)
flatpickr.localize(French)
}
else {
if (flatpickr.l10ns.en)
flatpickr.localize(flatpickr.l10ns.en)
}
// watch and change lang attribute of html on language change
watch(
locale,
@ -25,6 +36,15 @@ const _syncAppRtl = () => {
// Store selected language in cookie
storedLang.value = val as string
if (storedLang.value === 'fr') {
if (flatpickr.l10ns.fr)
flatpickr.localize(flatpickr.l10ns.fr)
}
else {
if (flatpickr.l10ns.en)
flatpickr.localize(flatpickr.l10ns.en)
}
// set isAppRtl value based on selected language
if (themeConfig.app.i18n.langConfig && themeConfig.app.i18n.langConfig.length) {
themeConfig.app.i18n.langConfig.forEach(lang => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1 +1,4 @@
// Write your overrides
.dt-row-striped tr:nth-of-type(even) {
background-color: #f6f2f28d;
}

View File

@ -2,7 +2,7 @@ import { createFetch } from '@vueuse/core'
import { destr } from 'destr'
export const useApi = createFetch({
baseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
baseUrl: import.meta.env.VITE_API_BASE_URL,
fetchOptions: {
headers: {
Accept: 'application/json',

View File

@ -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>

View File

@ -1,5 +1,24 @@
<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')
}
const avatarColor = computed(() => {
switch (useUserStore.role) {
// eslint-disable-next-line @stylistic/ts/indent
case 'Admin': return 'error'
// eslint-disable-next-line @stylistic/ts/indent
case 'Support': return 'secondary'
// eslint-disable-next-line @stylistic/ts/indent
default: return 'info'
}
})
</script>
<template>
@ -13,10 +32,18 @@ import avatar1 from '@images/avatars/avatar-1.png'
>
<VAvatar
class="cursor-pointer"
color="primary"
:color="avatarColor"
variant="tonal"
>
<VImg :src="avatar1" />
<template v-if="useUserStore.role === 'Admin'">
AD
</template>
<template v-else-if="useUserStore.role === 'Support'">
SU
</template>
<template v-else>
IN
</template>
<!-- SECTION Menu -->
<VMenu
@ -38,80 +65,33 @@ import avatar1 from '@images/avatars/avatar-1.png'
color="success"
>
<VAvatar
color="primary"
:color="avatarColor"
variant="tonal"
>
<VImg :src="avatar1" />
<template v-if="useUserStore.role === 'Admin'">
AD
</template>
<template v-else-if="useUserStore.role === 'Support'">
SU
</template>
<template v-else>
IN
</template>
</VAvatar>
</VBadge>
</VListItemAction>
</template>
<VListItemTitle class="font-weight-semibold">
John Doe
{{ useUserStore.username }}
</VListItemTitle>
<VListItemSubtitle>Admin</VListItemSubtitle>
<VListItemSubtitle>{{ useUserStore.role }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-user"
size="22"
/>
</template>
<VListItemTitle>Profile</VListItemTitle>
</VListItem>
<!-- 👉 Settings -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-settings"
size="22"
/>
</template>
<VListItemTitle>Settings</VListItemTitle>
</VListItem>
<!-- 👉 Pricing -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-currency-dollar"
size="22"
/>
</template>
<VListItemTitle>Pricing</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem link>
<template #prepend>
<VIcon
class="me-2"
icon="tabler-help"
size="22"
/>
</template>
<VListItemTitle>FAQ</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 Logout -->
<VListItem to="/login">
<VListItem @click="logoutAndRedirect">
<template #prepend>
<VIcon
class="me-2"

60
src/models/storeData.ts Normal file
View File

@ -0,0 +1,60 @@
interface Store {
id_structure: number
nom: string
ip_master: string
telephone: string
photoLink: string
enseigne: string
nbcaisses: number
pays: string
adresse: string
caisses: Caisse[]
date_migration: string
}
interface Caisse {
id_caisse: number
ip: string
}
interface storePos {
workstationId: number
ip: string
version: string
businessDate: Date
businessDateS: string
openingDate: string
closingDate: string
boTransaction: Transaction
replication: Replication
saleTransaction: xStoreTransaction
primaryRegister: boolean
fatalError: boolean
}
interface Replication {
pendingReplications: number
minPendingReplicationDate: string
maxPendingReplicationDate: string
}
interface Transaction {
backOfficeTransactions: number
minBackOfficeTransactionDate: string
maxBackOfficeTransactionDate: string
}
interface xStoreTransaction {
count: number
minDate: string
minDateT: string
minDateH: string
maxDate: string
maxDateT: string
maxDateH: string
}
export interface StoreData {
store: Store
pos: storePos[]
}

View File

@ -5,8 +5,38 @@ export default [
icon: { icon: 'tabler-smart-home' },
},
{
title: 'Second page',
to: { name: 'second-page' },
icon: { icon: 'tabler-file' },
title: 'Store',
to: { name: 'store-list' },
icon: { icon: 'tabler-building-store' },
},
{
title: 'Flow',
icon: { icon: 'tabler-topology-bus' },
children: [
{
title: 'BL not sent list',
to: { name: 'flux-bl-not-sent' },
icon: { icon: 'tabler-unlink' },
},
],
},
{
title: 'OBI',
icon: { icon: 'tabler-shopping-bag-check' },
},
{
title: 'Dotsoft',
icon: { icon: 'tabler-database-star' },
},
{
title: 'XADMIN',
icon: { icon: 'tabler-database-star' },
children: [
{
title: 'Logs',
to: { name: 'xadmin-log' },
icon: { icon: 'tabler-bug' },
},
],
},
]

View File

@ -5,8 +5,38 @@ export default [
icon: { icon: 'tabler-smart-home' },
},
{
title: 'Second page',
to: { name: 'second-page' },
icon: { icon: 'tabler-file' },
title: 'Store',
to: { name: 'store-list' },
icon: { icon: 'tabler-building-store' },
},
{
title: 'Flow',
icon: { icon: 'tabler-topology-bus' },
children: [
{
title: 'BL not sent list',
to: { name: 'flux-bl-not-sent' },
icon: { icon: 'tabler-unlink' },
},
],
},
{
title: 'OBI',
icon: { icon: 'tabler-shopping-bag-check' },
},
{
title: 'Dotsoft',
icon: { icon: 'tabler-database-star' },
},
{
title: 'XADMIN',
icon: { icon: 'tabler-database-star' },
children: [
{
title: 'Logs',
to: { name: 'xadmin-log' },
icon: { icon: 'tabler-bug' },
},
],
},
]

View File

@ -0,0 +1,310 @@
<script setup lang="ts">
import exportFromJSON from 'export-from-json'
import { VDataTable } from 'vuetify/labs/VDataTable'
const { t } = useI18n()
const headers = computed(() => [
{ title: t('Distributor'), key: 'libelleDis' },
{ title: t('Name'), key: 'nomStructure' },
{ title: t('Chain'), key: 'enseigne' },
{ title: t('Brand'), key: 'marque' },
{ title: t('Season'), key: 'codeSaison' },
{ title: t('R-C-S'), key: 'refRct' },
{ title: t('Item'), key: 'nomProduit' },
{ title: t('Item ID'), key: 'idProduit' },
{ title: t('External Code'), key: 'codeExterne' },
{ title: t('BL ID'), key: 'idBonLivraison' },
{ title: t('BL Date'), key: 'dateBl' },
{ title: t('Expeditor'), key: 'expediteur' },
{ title: t('Notes'), key: 'remarques' },
])
const selectedDistributor = ref()
const selectedStore = ref()
const selectedRefr = ref()
const searchQuery = ref<any>('')
// Data table options
const { data: dtListData } = await useApi<any>(createUrl('/flux/bl/notsent'))
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const distributor = computed(() => {
const allItems = dtListData.value.map(({ libelleDis }: { libelleDis: any }) => libelleDis)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((distributor: any, index: any, self: string | any[]) => self.indexOf(distributor) === index)
const sortedItems = uniqueItems.sort()
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedItems.map((distributor: any) => ({ title: distributor, value: distributor }))
})
const store = computed(() => {
const allItems = dtListData.value.map(({ nomStructure, idStructure }: { nomStructure: any; idStructure: any }) => ({ nomStructure, idStructure }))
const uniqueItems = allItems.filter((item: any, index: any, self: any[]) =>
// eslint-disable-next-line @typescript-eslint/no-shadow
index === self.findIndex(t => (
t.nomStructure === item.nomStructure && t.idStructure === item.idStructure
)),
)
const sortedItems = uniqueItems.sort((a: { idStructure: number }, b: { idStructure: number }) => a.idStructure - b.idStructure)
return sortedItems.map(({ nomStructure }: { nomStructure: any }) => ({ title: nomStructure, value: nomStructure }))
})
const filterRefr = computed(() => {
const allItems = dtListData.value.map(({ refR }: { refR: any }) => refR)
const uniqueItems = allItems.filter((refR: any, index: any, self: string | any[]) => self.indexOf(refR) === index)
const sortedItems = uniqueItems.sort()
return sortedItems.map((refR: any) => ({ title: refR, value: refR }))
})
const filteredData = computed(() => {
let filtered = dtListData.value
// If a distributor is selected, filter the records for this distributor
if (selectedDistributor.value)
filtered = filtered.filter(({ libelleDis }: { libelleDis: any }) => libelleDis === selectedDistributor.value)
// If a store is selected, filter the records for this store
if (selectedStore.value)
filtered = filtered.filter(({ nomStructure }: { nomStructure: any }) => nomStructure === selectedStore.value)
// If a ref R is selected, filter the records for this ref R
if (selectedRefr.value)
filtered = filtered.filter(({ refR }: { refR: any }) => refR === selectedRefr.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((dataFiltered: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(dataFiltered).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
const widgetData = computed(() => [
{ title: t('Distributors'), value: distributor.value.length, icon: 'tabler-brand-campaignmonitor' },
{ title: t('Stores'), value: store.value.length, icon: 'tabler-building-store' },
{ title: t('Items'), value: filterRefr.value.length, icon: 'tabler-shirt-sport' },
{ title: t('Errors'), value: dtListData.value.length, icon: 'tabler-exclamation-circle' },
])
const ExcelField = [
'idDistrib',
'libelleDis',
'idStructure',
'nomStructure',
'enseigne',
'marque',
'codeSaison',
'refR',
'refRc',
'refRct',
'nomProduit',
'idProduit',
'codeExterne',
'idBonLivraison',
'dateBl',
'idExpediteur',
'expediteur',
'remarques',
]
const ExcelData = dtListData.value.map((item: { [x: string]: string }) => {
const orderedItem: { [key: string]: string } = {}
ExcelField.forEach(field => {
orderedItem[field] = item[field]
})
return orderedItem
})
const exportEXCEL = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const fileName = `BLBLOQUES_${year}${month}${day}`
exportFromJSON({
data: ExcelData,
fileName,
exportType: exportFromJSON.types.xls,
})
}
</script>
<template>
<VBreadcrumbs
class="px-0 py-2"
:items="[
{ title: t('Home'), to: { name: 'root' } },
{ title: t('BL not sent list') },
]"
/>
<div>
<div>
<!-- 👉 Statistics Widgets -->
<VCard class="mb-6">
<VCardText>
<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">
<div class="d-flex flex-column gap-y-1">
<h4
class="text-h4 text-high-emphasis"
:class="{ 'text-error': id === 3 }"
>
{{ data.value }}
</h4>
<div class="text-body-1 text-capitalize">
{{ data.title }}
</div>
</div>
<VAvatar
color="rgba(var(--v-theme-on-background), var(--v-hover-opacity))"
rounded
class="text-high-emphasis"
size="38"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
: $vuetify.display.smAndUp ? id % 2 === 0
: false"
vertical
inset
/>
</template>
</VRow>
</VCardText>
</VCard>
<VCard
:title="$t('Filters')"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select distributor -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedDistributor"
:placeholder="$t('Distributor')"
:items="distributor"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select store -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedStore"
:placeholder="$t('Store')"
:items="store"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Reference R -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedRefr"
:placeholder="$t('Item')"
:items="filterRefr"
clearable
clear-icon="tabler-x"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- 👉 BL not sent -->
<VCard
:title="$t('BL not sent list')"
class="mb-6"
>
<div class="d-flex flex-wrap gap-4 mx-5">
<div class="d-flex align-center">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
:placeholder="$t('Search')"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-4 flex-wrap align-center">
<!-- 👉 Export button -->
<VBtn
variant="tonal"
color="primary"
prepend-icon="tabler-upload"
@click="exportEXCEL"
>
Export
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<!-- 👉 Datatable -->
<VDataTable
class="dt-row-striped"
:headers="headers"
:items="filteredData"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
/>
</VCol>
</Vcard>
</div>
</div>
</template>

View File

@ -2,24 +2,11 @@
<div>
<VCard
class="mb-6"
title="Kick start your project 🚀"
title="XSTORE HELPDESK DASHBOARD 🚀"
>
<VCardText>All the best for your new project.</VCardText>
<VCardText>
Please make sure to read our <a
href="https://demos.pixinvent.com/vuexy-vuejs-admin-template/documentation/"
target="_blank"
rel="noopener noreferrer"
class="text-decoration-none"
>
Template Documentation
</a> to understand where to go from here and how to use our template.
</VCardText>
</VCard>
<VCardText>A cet endroit bientôt des statistiques</VCardText>
<VCard title="Want to integrate JWT? 🔒">
<VCardText>We carefully crafted JWT flow so you can implement JWT with ease and with minimum efforts.</VCardText>
<VCardText>Please read our JWT Documentation to get more out of JWT authentication.</VCardText>
<VCardText>👉 Cliquez maintenant sur Boutique :)</VCardText>
</VCard>
</div>
</template>

View 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,67 @@ 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: '',
password: '',
})
const rememberMe = ref(false)
const isSnackbarVisibility = ref(false)
const useUserStore = userStore()
const login = async () => {
try {
await 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) : '/')
})
}
catch (err) {
// show invalid credentials error
isSnackbarVisibility.value = true
}
}
const onSubmit = () => {
refVForm.value?.validate()
.then(({ valid: isValid }) => {
if (isValid)
login()
})
}
</script>
<template>
@ -40,8 +78,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 +91,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 +112,51 @@ 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="() => { }">
<VForm
ref="refVForm"
@submit.prevent="onSubmit"
>
<VRow>
<!-- email -->
<!-- username -->
<VCol cols="12">
<AppTextField
v-model="form.email"
v-model="credentials.username"
label="Username"
placeholder="First name"
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 +166,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">

View File

@ -1,13 +0,0 @@
<template>
<div>
<VCard title="Create Awesome 🙌">
<VCardText>This is your second page.</VCardText>
<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.
</VCardText>
</VCard>
</div>
</template>

View File

@ -0,0 +1,90 @@
<script lang="ts" setup>
import StoreHeader from '@/views/pages/store/view/StoreHeader.vue'
import StoreTabAdmin from '@/views/pages/store/view/StoreTabAdmin.vue'
import StoreTabGeneral from '@/views/pages/store/view/StoreTabGeneral.vue'
import StoreTabItem from '@/views/pages/store/view/StoreTabItem.vue'
import StoreTabLog from '@/views/pages/store/view/StoreTabLog.vue'
import StoreTabRemote from '@/views/pages/store/view/StoreTabRemote.vue'
const { t } = useI18n()
const route = useRoute('store-details')
const storeTab = ref(route.query.tab || 'general')
// tabs
const tabs = [
{ title: 'General', icon: 'tabler-settings', tab: 'general' },
{ title: t('Item'), icon: 'tabler-shirt-sport', tab: 'item' },
{ title: t('Remote'), icon: 'tabler-brand-openvpn', tab: 'remote' },
{ title: 'Admin', icon: 'tabler-lock', tab: 'admin' },
{ title: 'Log', icon: 'tabler-bug', tab: 'log' },
]
const { data: storeData } = await useApi<any>(`/stores/${route.query.storeId}/details?dbHost=${route.query.dbHost}`)
</script>
<template>
<VBreadcrumbs
class="px-0 py-2"
:items="[
{ title: t('Home'), to: { name: 'root' } },
{ title: t('List of stores'), to: { name: 'store-list' } },
{ title: t('Store visualization') },
]"
/>
<div>
<StoreHeader
class="mb-5"
:store-data="storeData"
/>
<VTabs
v-model="storeTab"
class="v-tabs-pill"
>
<VTab
v-for="item in tabs"
:key="item.icon"
>
<VIcon
size="20"
start
:icon="item.icon"
/>
{{ item.title }}
</VTab>
</VTabs>
<VWindow
v-model="storeTab"
class="mt-6 disable-tab-transition"
:touch="false"
>
<!-- 👉 General -->
<VWindowItem>
<StoreTabGeneral :store-data="storeData" />
</VWindowItem>
<!-- 👉 Item -->
<VWindowItem>
<StoreTabItem />
</VWindowItem>
<!-- 👉 Remote Access -->
<VWindowItem>
<StoreTabRemote :store-data="storeData" />
</VWindowItem>
<!-- 👉 Admin -->
<VWindowItem>
<StoreTabAdmin :store-data="storeData" />
</VWindowItem>
<!-- 👉 Log -->
<VWindowItem>
<StoreTabLog :store-data="storeData" />
</VWindowItem>
</VWindow>
</div>
</template>

View File

@ -0,0 +1,364 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { VDataTable } from 'vuetify/labs/VDataTable'
import { useDataTableStore } from '@/stores/datatable.store'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const headers = computed(() => [
{ title: 'ID', key: 'id_structure' },
{ title: t('Name'), key: 'nom' },
{ title: t('Migration'), key: 'date_migration' },
{ title: 'Pos', key: 'nbcaisses' },
{ title: 'IP', key: 'ip_master', sortable: false },
{ title: t('Phone'), key: 'telephone', sortable: false },
{ title: t('Brand'), key: 'enseigne' },
{ title: t('Country'), key: 'pays' },
{ title: '', key: 'actions', sortable: false },
])
const selectedCountry = ref()
const selectedBrand = ref()
const selectedNbPos = ref()
// for reload process
const storesList = ref<any>('')
const isLoading = ref(false)
const searchQuery = ref<any>('')
// Data table options
const { data: dtListData } = await useApi<any>(createUrl('/stores'))
storesList.value = dtListData.value
const country = computed(() => {
const allItems = storesList.value.map((store: { pays: any }) => store.pays)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((country: any, index: any, self: string | any[]) => self.indexOf(country) === index)
const sortedItems = uniqueItems.sort()
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedItems.map((country: any) => ({ title: country, value: country }))
})
const brand = computed(() => {
const allItems = storesList.value.map((store: { enseigne: any }) => store.enseigne)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((brand: any, index: any, self: string | any[]) => self.indexOf(brand) === index)
const sortedItems = uniqueItems.sort()
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedItems.map((brand: any) => ({ title: brand, value: brand }))
})
const nbPos = computed(() => {
const allItems = storesList.value.map((store: { nbcaisses: number }) => store.nbcaisses)
const uniqueItems = Array.from(new Set(allItems)) as number[] // Utilisez une assertion de type
const sortedItems = uniqueItems.sort((a, b) => a - b) // Triez les nombres en ordre croissant
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedItems.map((nbPos: number) => ({ title: nbPos, value: nbPos }))
})
const filteredData = computed(() => {
let filtered = storesList.value
// If a brand is selected, filter the records for this brand
if (selectedBrand.value)
filtered = filtered.filter((store: { enseigne: any }) => store.enseigne === selectedBrand.value)
// If a country is selected, filter the records for this country
if (selectedCountry.value)
filtered = filtered.filter((store: { pays: any }) => store.pays === selectedCountry.value)
// If a number of POS is selected, filter the records for this number
if (selectedNbPos.value)
filtered = filtered.filter((store: { nbcaisses: any }) => store.nbcaisses === selectedNbPos.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((store: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(store).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
const selectedStore = ref(null)
const isDialogVisible = ref(false)
const openPosList = (store: any) => {
selectedStore.value = store
isDialogVisible.value = true
}
interface Store {
id_structure: number
caisses: Caisse[]
}
interface Caisse {
id_caisse: number
ip: string
}
const reloadStores = async () => {
isLoading.value = true
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data, error } = await useApi<any>(createUrl('/stores/reload'))
if (!isEmpty(error.value))
console.error('Error loading store data:', error.value)
// Reload the store data
const { data: storeData, error: storeError } = await useApi<any>(createUrl('/stores'))
if (!isEmpty(storeError.value))
console.error('Error loading store data:', storeError.value)
else
storesList.value = storeData.value
isLoading.value = false
}
// pinia store for datatable filters & options
const stStoreList = useDataTableStore()
const saveDtFilters = () => {
stStoreList.setFilters('selectedCountry', selectedCountry.value)
stStoreList.setFilters('selectedBrand', selectedBrand.value)
stStoreList.setFilters('selectedNbPos', selectedNbPos.value)
stStoreList.searchText = searchQuery.value
}
const restoreDtFilters = () => {
if (stStoreList.filters.selectedCountry)
selectedCountry.value = stStoreList.filters.selectedCountry
if (stStoreList.filters.selectedBrand)
selectedBrand.value = stStoreList.filters.selectedBrand
if (stStoreList.filters.selectedNbPos)
selectedNbPos.value = stStoreList.filters.selectedNbPos
if (stStoreList.searchText)
searchQuery.value = stStoreList.searchText
}
const navigateToStoreDetails = (item: { ip_master: string; id_structure: number }) => {
// Save datatable filters state
saveDtFilters()
// go to store details
router.push(`/store/details?dbHost=${item.ip_master}&storeId=${item.id_structure}`)
}
onMounted(() => {
// Restore datatable filters state on page load
restoreDtFilters()
})
watch(route, (to, from) => {
if (from && from.path !== '/store/details')
stStoreList.clearState()
}, { immediate: true })
</script>
<template>
<VBreadcrumbs
class="px-0 py-2"
:items="[
{ title: t('Home'), to: { name: 'root' } },
{ title: t('List of stores') },
]"
/>
<div>
<!-- 👉 filters -->
<VCard
:title="$t('Filters')"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select country -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedCountry"
:placeholder="$t('Country')"
:items="country"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Brand -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedBrand"
:placeholder="$t('Brand')"
:items="brand"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Multi POS -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedNbPos"
:placeholder="$t('Multi POS')"
:items="nbPos"
clearable
clear-icon="tabler-x"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- 👉 datatable, search & export & reload -->
<VCard
:title="$t('List of stores')"
class="mb-6"
>
<div class="d-flex flex-wrap gap-4 mx-5">
<div class="d-flex align-center">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
:placeholder="$t('Search')"
density="compact"
style="inline-size: 200px;"
type="text"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-4 flex-wrap align-center">
<VBtn
color="primary"
prepend-icon="tabler-reload"
@click="reloadStores"
>
{{ t("Reload") }}
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable https://vuetifyjs.com/en/api/v-data-table/#props -->
<VDataTable
v-else
:headers="headers"
:items="filteredData"
:items-per-page="stStoreList.itemsPerPage"
:page="stStoreList.currentPage"
:sort-by="stStoreList.getSortBy()"
@update:sort-by="sortBy => { stStoreList.setSortBy(sortBy[0]?.key) ; stStoreList.setSortOrder(sortBy[0]?.order) }"
@update:items-per-page="itemsPerPage => { stStoreList.setItemsPerPage(itemsPerPage) }"
@update:page="page => { stStoreList.setCurrentPage(page) }"
>
<!-- Store details hyperlink -->
<template #item.nom="{ item }">
<a
href="#"
class="router-link"
@click.prevent="navigateToStoreDetails(item)"
>
{{ item.nom }}
</a>
</template>
<!-- Pos count -->
<template #item.nbcaisses="{ item }">
<VIcon v-if="item.nbcaisses > 1 && item.nbcaisses <= 9">
{{ `tabler-square-rounded-number-${item.nbcaisses}` }}
</VIcon>
<span v-else-if="item.nbcaisses > 9">{{ item.nbcaisses }}</span>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
<VMenu activator="parent">
<VList>
<VListItem
value="connect"
prepend-icon="tabler-numbers"
@click="openPosList(item)"
>
POS
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
</VDataTable>
<!-- 👉 Dialog page for line actions -->
<VDialog
v-model="isDialogVisible"
max-width="600"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="isDialogVisible = !isDialogVisible" />
<!-- Dialog Content -->
<VCard :title="t('Connect to invidual POS')">
<VCardText>
<div v-if="selectedStore && (selectedStore as Store).caisses">
<div
v-for="(caisse, index) in (selectedStore as Store).caisses"
:key="index"
class="d-flex align-items-center mb-5"
>
<VTextField
v-model="caisse.ip"
label="IP"
class="me-3"
/>
<VBtn
:to="`/store/details?dbHost=${caisse.ip}&storeId=${(selectedStore as Store).id_structure}&workstationId=${caisse.id_caisse}`"
color="primary"
>
Pos {{ caisse.id_caisse }}
</VBtn>
</div>
</div>
</VCardText>
<VCardText class="d-flex justify-end flex-wrap gap-3">
<VBtn
variant="tonal"
color="secondary"
@click="isDialogVisible = false"
>
{{ $t("Close") }}
</VBtn>
</VCardText>
</VCard>
</VDialog>
</VCol>
</VCard>
</div>
</template>

View File

@ -0,0 +1,492 @@
<script setup lang="ts">
import { endOfDay, format } from 'date-fns'
import exportFromJSON from 'export-from-json'
import { VDataTable } from 'vuetify/labs/VDataTable'
const { t } = useI18n()
const headers = computed(() => [
{ title: '', key: 'data-table-expand' },
{ title: t('Store'), key: 'storeName' },
{ title: t('WSK'), key: 'wkstnId' },
{ title: t('Level'), key: 'logLevel' },
{ title: t('Category'), key: 'loggerCategory' },
{ title: t('Thread'), key: 'threadName' },
{ title: t('Date'), key: 'createDate' },
{ title: t('User'), key: 'createUserId' },
])
const selectedStore = ref(null)
const selectedWkStn = ref(null)
const selectedLogLevel = ref(null)
const selectedCategory = ref(null)
const selectedDateRange = ref(null)
const searchQuery = ref('')
const isSnackbarExport = ref(false)
const isSnackbarRange = ref(false)
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: ['createDate'], sortDesc: [true] })
const isLoading = ref(false)
interface dtListDataType {
businessDate: string
createDate: Date
createUserId: string
rtlLocId: number
wkstnId: number
logLevel: string
threadName: string
logMessage: string
loggerCategory: string
storeName: string
}
const dtListData = ref<dtListDataType[]>([])
const levelCounts = reactive({ INFO: 0, WARN: 0, ERROR: 0, FATAL: 0 })
const widgetData = computed(() => [
{ title: 'INFO', value: levelCounts.INFO, icon: 'tabler-info-circle' },
{ title: 'WARN', value: levelCounts.WARN, icon: 'tabler-alert-triangle' },
{ title: 'ERRROR', value: levelCounts.ERROR, icon: 'tabler-exclamation-circle' },
{ title: 'FATAL', value: levelCounts.FATAL, icon: 'tabler-bug' },
])
const fetchData = async () => {
isLoading.value = true
isSnackbarExport.value = false
isSnackbarRange.value = false
const rangeDate = selectedDateRange.value || ''
let beginDate
let endDate
if (rangeDate.length === 8) {
beginDate = rangeDate.substring(0, 8)
endDate = beginDate
}
else if (rangeDate.length > 0) {
beginDate = rangeDate.substring(0, 8)
endDate = rangeDate.substring(rangeDate.length - 8)
}
else {
beginDate = format(endOfDay(new Date()), 'yyyyMMdd')
endDate = beginDate
}
let paramUrl = `?beginDate=${beginDate}&endDate=${endDate}`
if (selectedStore && selectedStore.value) {
const matchedStore: dtListDataType | undefined = dtListData.value.find((item: dtListDataType) => item.storeName === selectedStore.value)
if (matchedStore && matchedStore.rtlLocId)
paramUrl += `&storeId=${matchedStore.rtlLocId}`
}
if (selectedWkStn && selectedWkStn.value)
paramUrl += `&wkstnId=${selectedWkStn.value}`
if (searchQuery.value)
paramUrl += `&searchText=${searchQuery.value}`
levelCounts.INFO = 0
levelCounts.WARN = 0
levelCounts.ERROR = 0
levelCounts.FATAL = 0
if (beginDate !== endDate && (searchQuery.value.length < 5)) {
isSnackbarRange.value = true
dtListData.value = []
}
else {
const response = await useApi<any>(createUrl(`/xadmin/log${paramUrl}`))
if (response.data.value) {
dtListData.value = response.data.value
// Calculate the number of occurrences of each level
dtListData.value.forEach((item: { logLevel: string }) => {
const logLevel = item.logLevel as keyof typeof levelCounts
if (logLevel in levelCounts)
levelCounts[logLevel]++
})
}
else { dtListData.value = [] }
}
isLoading.value = false
}
const store = computed(() => {
const allItems = dtListData.value.map(({ storeName, rtlLocId }: { storeName: any; rtlLocId: any }) => ({ storeName, rtlLocId }))
const uniqueItems = allItems.filter((item: any, index: any, self: any[]) =>
// eslint-disable-next-line @typescript-eslint/no-shadow
index === self.findIndex(t => (
t.storeName === item.storeName && t.rtlLocId === item.rtlLocId
)),
)
const sortedItems = uniqueItems.sort((a: { rtlLocId: number }, b: { rtlLocId: number }) => a.rtlLocId - b.rtlLocId)
return sortedItems.map(({ storeName }: { storeName: any }) => ({ title: storeName, value: storeName }))
})
const wkstn = computed(() => {
const allItems = dtListData.value.map((list: { wkstnId: any }) => list.wkstnId)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((wkstn: any, index: any, self: string | any[]) => self.indexOf(wkstn) === index)
const sortedItems = uniqueItems.sort()
return sortedItems.map((list: any) => ({ title: list, value: list }))
})
const level = computed(() => {
const allItems = dtListData.value.map((list: { logLevel: any }) => list.logLevel)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((level: any, index: any, self: string | any[]) => self.indexOf(level) === index)
const sortedItems = uniqueItems.sort()
return sortedItems.map((list: any) => ({ title: list, value: list }))
})
const category = computed(() => {
const allItems = dtListData.value.map((list: { loggerCategory: any }) => list.loggerCategory)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueItems = allItems.filter((category: any, index: any, self: string | any[]) => self.indexOf(category) === index)
const sortedItems = uniqueItems.sort()
return sortedItems.map((list: any) => ({ title: list, value: list }))
})
const filteredLogList = computed(() => {
let filtered = dtListData.value
// If a store is selected, filter the records for this store
if (selectedStore.value !== undefined && selectedStore.value !== null)
filtered = filtered.filter((list: { storeName: any }) => list.storeName === selectedStore.value)
// If a workstation is selected, filter the records for this number
if (selectedWkStn.value !== undefined && selectedWkStn.value !== null)
filtered = filtered.filter((list: { wkstnId: any }) => list.wkstnId === selectedWkStn.value)
// If a loglevel is selected, filter the records for this Loglevel
if (selectedLogLevel.value !== undefined && selectedLogLevel.value !== null)
filtered = filtered.filter((list: { logLevel: any }) => list.logLevel === selectedLogLevel.value)
// If a loggerCategory is selected, filter the records for this loggerCategory
if (selectedCategory.value !== undefined && selectedCategory.value !== null)
filtered = filtered.filter((list: { loggerCategory: any }) => list.loggerCategory === selectedCategory.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((log: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(log).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
const ExcelField = [
'rtlLocId',
'storeName',
'wkstnId',
'logLevel',
'threadName',
'logMessage',
'loggerCategory',
'createDate',
'createUserId',
]
const ExcelData = computed(() => {
return dtListData.value.map((item: dtListDataType) => {
const orderedItem: { [key: string]: string } = {}
ExcelField.forEach(field => {
if (field === 'createDate') {
const date = new Date(item[field as keyof dtListDataType])
orderedItem[field] = format(date, 'dd/MM/yyyy HH:mm:ss')
}
else {
orderedItem[field] = String(item[field as keyof dtListDataType])
}
})
return orderedItem
})
})
const exportEXCEL = () => {
const fileName = `LOGS_XADMIN-${format(new Date(), 'yyyyMMdd')}`
if (ExcelData.value.length > 0) {
exportFromJSON({
data: ExcelData.value,
fileName,
exportType: exportFromJSON.types.xls,
})
}
else {
isSnackbarExport.value = true
}
}
</script>
<template>
<VBreadcrumbs
class="px-0 py-2"
:items="[
{ title: t('Home'), to: { name: 'root' } },
{ title: t('Logs list') },
]"
/>
<div>
<!-- 👉 Statistics Widgets -->
<VCard class="mb-6">
<VCardText>
<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">
<div class="d-flex flex-column gap-y-1">
<h4
class="text-h4 text-high-emphasis"
:class="{ 'text-error': id === 3 }"
>
{{ data.value }}
</h4>
<div class="text-body-1 text-capitalize">
{{ data.title }}
</div>
</div>
<VAvatar
color="rgba(var(--v-theme-on-background), var(--v-hover-opacity))"
rounded
class="text-high-emphasis"
size="38"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
: $vuetify.display.smAndUp ? id % 2 === 0
: false"
vertical
inset
/>
</template>
</VRow>
</VCardText>
</VCard>
<!-- 👉 filters -->
<VCard
:title="$t('Filters')"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select workstation -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedStore"
:placeholder="$t('Store')"
:items="store"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select workstation -->
<VCol
cols="12"
sm="2"
>
<AppSelect
v-model="selectedWkStn"
:placeholder="$t('WorkStation')"
:items="wkstn"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select log level -->
<VCol
cols="12"
sm="2"
>
<AppSelect
v-model="selectedLogLevel"
:placeholder="$t('Level')"
:items="level"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select logger category -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedCategory"
:placeholder="$t('loggerCategory')"
:items="category"
clearable
clear-icon="tabler-x"
/>
</VCol>
</VRow>
<VRow>
<!-- 👉 Date log -->
<VCol sm="4">
<AppDateTimePicker
v-model="selectedDateRange"
label=""
:placeholder="$t('Select date')"
:config="{ mode: 'range', altFormat: 'J M Y', altInput: true, dateFormat: 'Ymd' }"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- 👉 datatable, search & export & reload -->
<VCard
:title="$t('Logs list')"
class="mb-6"
>
<div class="d-flex flex-wrap gap-4 mx-5">
<div class="d-flex align-center">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
:placeholder="$t('Search')"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-1 flex-wrap aalign-center">
<!-- 👉 Export button -->
<VBtn
variant="tonal"
color="primary"
prepend-icon="tabler-upload"
@click="exportEXCEL"
>
Export
</VBtn>
</div>
<div class="d-flex gap-4 flex-wrap align-center">
<VBtn
color="primary"
prepend-icon="tabler-reload"
@click="fetchData"
>
{{ t("Reload") }}
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable -->
<VDataTable
v-else
class="dt-row-striped"
:headers="headers"
:items="filteredLogList"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
expand-on-click
>
<!-- format date log -->
<template #item.createDate="{ item }">
{{ format(new Date(item.createDate), 'dd/MM/yyyy HH:mm:ss') }}
</template>
<!-- format business date -->
<template #item.businessDate="{ item }">
{{ format(new Date(item.businessDate), 'dd/MM/yyyy') }}
</template>
<!-- Expanded Row Data -->
<template #expanded-row="slotProps">
<tr class="v-data-table__tr">
<td :colspan="headers.length">
<p class="my-1">
{{ slotProps.item.logMessage }}
</p>
</td>
</tr>
</template>
</VDataTable>
</VCol>
</VCard>
</div>
<!-- Snackbar Export -->
<VSnackbar
v-model="isSnackbarExport"
location="center"
>
{{ $t('ExcelData is empty, cannot export') }}
<template #actions>
<VBtn
color="error"
@click="isSnackbarExport = false"
>
{{ $t("Close") }}
</VBtn>
</template>
</VSnackbar>
<!-- Snackbar Export -->
<VSnackbar
v-model="isSnackbarRange"
location="center"
>
{{ $t('Dates range error') }}
<template #actions>
<VBtn
color="error"
@click="isSnackbarRange = false"
>
{{ $t("Close") }}
</VBtn>
</template>
</VSnackbar>
</template>

47
src/plugins/1.router/guards.ts Executable file
View File

@ -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 */
}
})
}

10
src/plugins/casl/ability.ts Executable file
View File

@ -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]>()

View File

@ -0,0 +1,4 @@
import { useAbility as useCaslAbility } from '@casl/vue'
import type { ability } from '../ability'
export const useAbility = () => useCaslAbility<typeof ability>()

14
src/plugins/casl/index.ts Executable file
View File

@ -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,
})
}

8
src/plugins/casl/shims-ability.d.ts vendored Executable file
View File

@ -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;
}
}

View File

@ -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)
}

View File

@ -0,0 +1 @@
export const paginateArray = (array: unknown[], perPage: number, page: number) => array.slice((page - 1) * perPage, page * perPage)

View File

@ -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`
}

29
src/plugins/i18n/index.ts Executable file
View File

@ -0,0 +1,29 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { cookieRef } from '@layouts/stores/config'
import { themeConfig } from '@themeConfig'
const messages = Object.fromEntries(
Object.entries(
import.meta.glob<{ default: any }>('./locales/*.json', { eager: true }))
.map(([key, value]) => [key.slice(10, -5), value.default]),
)
let _i18n: any = null
export const getI18n = () => {
if (_i18n === null) {
_i18n = createI18n({
legacy: false,
locale: cookieRef('language', themeConfig.app.i18n.defaultLocale).value,
fallbackLocale: 'en',
messages,
})
}
return _i18n
}
export default function (app: App) {
app.use(getI18n())
}

282
src/plugins/i18n/locales/ar.json Executable file
View File

@ -0,0 +1,282 @@
{
"Home": "الصفحة الرئيسية",
"Store": "متجر",
"Stores": "المتاجر",
"Flow": "التدفق",
"OBI": "OBI",
"Dotsoft": "Dotsoft",
"List of stores": "قائمة المتاجر",
"Store visualization": "عرض المتاجر",
"BL not sent list": "قائمة البيانات الخارجية غير المرسلة",
"BL not sent": "بيانات البيانات الخارجية غير المرسلة",
"ST ID": "ST ID",
"Exp ID": "Exp ID",
"BL ID": "BL ID",
"BL Date": "تاريخ BL",
"External Code": "رمز خارجي",
"Notes": "ملاحظات",
"Expeditor": "المندوب",
"Distributor": "البائع",
"Distributors": "البائعين",
"Season": "الموسم",
"Country": "الدولة",
"Brand": "العلامة التجارية",
"R-C-S": "R-C-S",
"Item ID": "معرف العنصر",
"Chain": "المجموعة",
"Multi POS": "Multi POS",
"Search": "بحث",
"Name": "الاسم",
"Phone": "هاتف",
"Mode": "الوضع",
"Value": "القيمة",
"Item": "العنصر",
"Items": "العناصر",
"Remote": "متصل",
"Option": "خيار",
"Stock": "المخزون",
"Price": "السعر",
"ITEM_ID": "ID العنصر",
"PRICE": "السعر",
"TYPE": "TYPE",
"PARENT": "PARENT",
"LEVEL": "LEVEL",
"Migration": "MIGRATION",
"EFFECTIVE DATE": "EFFECTIVE DATE",
"EXPIRATION DATE": "EXPIRATION DATE",
"DATE_CREATE": "CREATE DATE",
"USER_CREATE": "CREATE USER",
"DATE_UPDATE": "UPDATE DATE",
"USER_UPDATE": "UPDATE USER",
"Reload": "تحديث",
"WorkStation": "القطعة العملية",
"SignatureString": "Signature string",
"SignatureSource": "Signature source",
"Submit": "Submit",
"You can search for a reference, reference-color or reference-color-size.": "You can search for a reference, reference-color or reference-color-size.",
"Minimum 5 characters long": "Minimum 5 characters long",
"Filters": "Filters",
"XADMIN": "XADMIN",
"Logs": "Logs",
"Level": "level",
"loggerCategory": "loggerCategory",
"Thread": "Thread",
"Date": "Date",
"WSK": "WSK",
"Select date": "Select date",
"ExcelData is empty, cannot export": "ExcelData is empty, cannot export",
"Logs list": "Logs list",
"Business Date": "Business Date",
"Dates range error": "With a date range, a text of at least 5 characters must be specified",
"Close": "Close",
"Connect to invidual POS": "Connect to invidual POS",
"---------------------------": "---------------------------",
"UI Elements": "عناصر واجهة المستخدم",
"Forms & Tables": "النماذج والجداول",
"Pages": "الصفحات",
"Charts & Maps": "الرسوم البيانية والخرائط",
"Others": "آحرون",
"Typography": "الطباعة",
"Cards": "البطاقات",
"Basic": "أساسي",
"Advance": "يتقدم",
"Widgets": "الحاجيات",
"Actions": "أجراءات",
"Components": "عناصر",
"Alert": "انذار",
"Close Alert": "أغلق التنبيه",
"Avatar": "الصورة الرمزية",
"Badge": "شارة",
"Button": "زر",
"Calendar": "تقويم",
"Image": "صورة",
"Pagination": "ترقيم الصفحات",
"Progress Circular": "تقدم التعميم",
"Progress Linear": "تقدم خطي",
"Autocomplete": "الإكمال التلقائي",
"Tooltip": "تلميح",
"Slider": "المنزلق",
"Date Time Picker": "منتقي التاريخ والوقت",
"Select": "يختار",
"Switch": "يُحوّل",
"Checkbox": "خانة اختيار",
"Radio": "مذياع",
"Textarea": "تيكستاريا",
"Rating": "تقييم",
"File Input": "إدخال الملف",
"Otp Input": "إدخال أوتب",
"Form Layout": "تخطيط النموذج",
"Form Validation": "التحقق من صحة النموذج",
"Charts": "الرسوم البيانية",
"Apex Chart": "مخطط أبيكس",
"Chartjs": "تشارتجس",
"Account Settings": "إعدادت الحساب",
"User Profile": "ملف تعريفي للمستخدم",
"FAQ": "التعليمات",
"Dialog Examples": "أمثلة على الحوار",
"Pricing": "التسعير",
"List": "قائمة",
"Edit": "يحرر",
"Nav Levels": "مستويات التنقل",
"Level 2.1": "المستوى 2.1",
"Level 2.2": "مستوى 2.2",
"Level 3.1": "المستوى 3.1",
"Level 3.2": "المستوى 3.2",
"Raise Support": "رفع الدعم",
"Documentation": "توثيق",
"Dashboards": "لوحات القيادة",
"Apps & Pages": "التطبيقات والصفحات",
"Email": "البريد الإلكتروني",
"Chat": "دردشة",
"Invoice": "فاتورة",
"Preview": "معاينة",
"Add": "يضيف",
"User": "المستعمل",
"View": "رأي",
"Login v1": "تسجيل الدخول v1",
"Login v2": "تسجيل الدخول v2",
"Login": "تسجيل الدخول",
"Register v1": "تسجيل v1",
"Register v2": "تسجيل v2",
"Register": "تسجيل",
"Forget Password v1": "نسيت كلمة المرور v1",
"Forget Password v2": "نسيت كلمة المرور v2",
"Forgot Password v1": "نسيت كلمة المرور v1",
"Forgot Password v2": "نسيت كلمة المرور v2",
"Forgot Password": "نسيت كلمة المرور",
"Reset Password v1": "إعادة تعيين كلمة المرور v1",
"Reset Password v2": "إعادة تعيين كلمة المرور v2",
"Reset Password": "إعادة تعيين كلمة المرور",
"Miscellaneous": "متفرقات",
"Coming Soon": "قريبا",
"Not Authorized": "غير مخول",
"Under Maintenance": "تحت الصيانة",
"Error": "خطأ",
"Errors": "خطوات",
"Statistics": "إحصائيات",
"Analytics": "تحليلات",
"Access Control": "صلاحية التحكم صلاحية الدخول",
"User Interface": "واجهة المستخدم",
"CRM": "سي آر إم",
"Icons": "أيقونات",
"Chip": "رقاقة",
"Dialog": "حوار",
"Expansion Panel": "لوحة التوسع",
"Combobox": "صندوق التحرير",
"Textfield": "مجال التحرير مكان كتابة النص",
"Range Slider": "نطاق المنزلق",
"Menu": "قائمة الطعام",
"Snackbar": "مطعم الوجبات الخفيفة",
"Tabs": "نوافذ التبويب",
"Form Elements": "عناصر النموذج",
"Form Layouts": "تخطيطات النموذج",
"Authentication": "المصادقة",
"Page Not Found - 404": "الصفحة غير موجودة - 404",
"Not Authorized - 401": "غير مصرح - 401",
"Server Error - 500": "خطأ في الخادم - 500",
"2": "2",
"Forms": "نماذج",
"Timeline": "الجدول الزمني",
"Disabled Menu": "قائمة المعوقين",
"Help Center": "مركز المساعدة",
"Verify Email": "التحقق من البريد الإلكتروني",
"Verify Email v1": "تحقق من البريد الإلكتروني v1",
"Verify Email v2": "تحقق من البريد الإلكتروني v2",
"Two Steps": "خطوتين",
"Two Steps v1": "خطوتين v1.0",
"Two Steps v2": "خطوتين v2.0",
"Custom Input": "إدخال مخصص",
"Extensions": "ملحقات",
"Tour": "رحلة",
"Register Multi-Steps": "تسجيل خطوات متعددة",
"Wizard Examples": "أمثلة المعالج",
"Checkout": "الدفع",
"Create Deal": "إنشاء صفقة",
"Property Listing": "قائمة الممتلكات ",
"Roles & Permissions": "الأدوار والأذونات",
"Roles": "الأدوار",
"Permissions": "الأذونات",
"Simple Table": "جدول بسيط",
"Tables": "الجداول",
"DataTable": "جدول البيانات",
"Data Table": "جدول البيانات",
"Apps": "التطبيقات",
"Misc": "متفرقات",
"Wizard Pages": "صفحات المعالج",
"eCommerce": "التجارة الإلكترونية",
"Form Wizard": "معالج النموذج",
"Numbered": "مرقم",
"ecommerce": "التجارة الإلكترونية",
"Ecommerce": "التجارة الإلكترونية",
"Product": "المنتج",
"Category": "الفئة",
"Order": "طلب",
"Details": "تفاصيل",
"Customer": "الزبون",
"Manage Review": "إدارة المراجعة",
"Referrals": "الإحالات",
"Settings": "الإعدادات",
"Course Details": "تفاصيل الدورة التدريبية",
"My Course": "دورتي",
"Overview": "نظرة عامة",
"Academy": "أكاديمية",
"Logistics": "الخدمات اللوجستية",
"Dashboard": "لوحة القيادة",
"Fleet": "الأسطول",
"Editors": "المحررين",
"Front Pages": "الصفحات الأمامية",
"Landing": "المقصودة",
"checkout": "الدفع",
"Payment": "دفع",
"Swiper": "المنزلق",
"3": "3",
"5": "5",
"10": "10",
"20": "20",
"25": "25",
"50": "50",
"100": "100",
"$vuetify": {
"badge": "شارة",
"noDataText": "لا تتوافر بيانات",
"close": "قريب",
"open": "افتح",
"carousel": {
"ariaLabel": {
"delimiter": "تحديد"
}
},
"dataFooter": {
"itemsPerPageText": "مواد لكل صفحة:",
"itemsPerPageAll": "الجميع",
"pageText": "{0} - {1} من {2}",
"firstPage": "الصفحة الأولى",
"prevPage": "الصفحة السابقة",
"nextPage": "الصفحة التالية",
"lastPage": "آخر صفحة"
},
"pagination": {
"ariaLabel": {
"root": "جذر",
"previous": "السابق",
"next": "التالي",
"currentPage": "الصفحه الحاليه",
"page": "صفحة"
}
},
"input": {
"clear": "صافي",
"appendAction": "إلحاق الإجراء",
"prependAction": "قبل العمل",
"otp": "أوتب"
},
"fileInput": {
"counterSize": "حجم العداد"
},
"rating": {
"ariaLabel": {
"item": "العنصر"
}
}
}
}

282
src/plugins/i18n/locales/en.json Executable file
View File

@ -0,0 +1,282 @@
{
"Home": "Home",
"Store": "Store",
"Stores": "Stores",
"Flow": "Flow",
"OBI": "OBI",
"Dotsoft": "Dotsoft",
"List of stores": "List of stores",
"Store visualization": "Store visualization",
"BL not sent list": "BL not sent list",
"BL not sent": "BL not sent",
"ST ID": "ST ID",
"Exp ID": "Exp ID",
"BL ID": "BL ID",
"BL Date": "BL Date",
"External Code": "External Code",
"Notes": "Notes",
"Expeditor": "Expeditor",
"Distributor": "Distributor",
"Distributors": "Distributors",
"Season": "Season",
"Country": "Country",
"Brand": "Brand",
"R-C-S": "R-C-S",
"Item ID": "Item ID",
"Chain": "Chain",
"Multi POS": "Multi POS",
"Search": "Search",
"Name": "Name",
"Phone": "Phone",
"Mode": "Mode",
"Value": "Value",
"Item": "Item",
"Items": "Items",
"Remote": "Remote",
"Option": "Option",
"Stock": "Stock",
"Price": "Price",
"ITEM_ID": "ITEM ID",
"PRICE": "PRICE",
"TYPE": "TYPE",
"PARENT": "PARENT",
"LEVEL": "LEVEL",
"Migration": "MIGRATION",
"EFFECTIVE DATE": "EFFECTIVE DATE",
"EXPIRATION DATE": "EXPIRATION DATE",
"DATE_CREATE": "CREATE DATE",
"USER_CREATE": "CREATE USER",
"DATE_UPDATE": "UPDATE DATE",
"USER_UPDATE": "UPDATE USER",
"Reload": "Reload",
"WorkStation": "Workstation",
"SignatureString": "Signature string",
"SignatureSource": "Signature source",
"Submit": "Submit",
"You can search for a reference, reference-color or reference-color-size.": "You can search for a reference, reference-color or reference-color-size.",
"Minimum 5 characters long": "Minimum 5 characters long",
"Filters": "Filters",
"XADMIN": "XADMIN",
"Logs": "Logs",
"Level": "level",
"loggerCategory": "loggerCategory",
"Thread": "Thread",
"Date": "Date",
"WSK": "WSK",
"Select date": "Select date",
"ExcelData is empty, cannot export": "No data, cannot export",
"Logs list": "Logs list",
"Business Date": "Business Date",
"Dates range error": "With a date range, a text of at least 5 characters must be specified",
"Close": "Close",
"Connect to invidual POS": "Connect to invidual POS",
"---------------------------": "---------------------------",
"UI Elements": "UI Elements",
"Forms & Tables": "Forms & Tables",
"Pages": "Pages",
"Charts & Maps": "Charts & Maps",
"Others": "Others",
"Typography": "Typography",
"Cards": "Cards",
"Basic": "Basic",
"Advance": "Advance",
"Widgets": "Widgets",
"Components": "Components",
"Alert": "Alert",
"Close Alert": "Close Alert",
"Avatar": "Avatar",
"Badge": "Badge",
"Button": "Button",
"Calendar": "Calendar",
"Image": "Image",
"Pagination": "Pagination",
"Progress Circular": "Progress Circular",
"Progress Linear": "Progress Linear",
"Autocomplete": "Autocomplete",
"Tooltip": "Tooltip",
"Slider": "Slider",
"Date Time Picker": "Date Time Picker",
"Select": "Select",
"Switch": "Switch",
"Checkbox": "Checkbox",
"Radio": "Radio",
"Textarea": "Textarea",
"Rating": "Rating",
"File Input": "File Input",
"Otp Input": "Otp Input",
"Form Layout": "Form Layout",
"Form Validation": "Form Validation",
"Charts": "Charts",
"Apex Chart": "Apex Chart",
"Chartjs": "Chartjs",
"Account Settings": "Account Settings",
"User Profile": "User Profile",
"FAQ": "FAQ",
"Dialog Examples": "Dialog Examples",
"Pricing": "Pricing",
"List": "List",
"Edit": "Edit",
"Nav Levels": "Nav Levels",
"Level 2.1": "Level 2.1",
"Level 2.2": "Level 2.2",
"Level 3.1": "Level 3.1",
"Level 3.2": "Level 3.2",
"Raise Support": "Raise Support",
"Documentation": "Documentation",
"Dashboards": "Dashboards",
"Analytics": "Analytics",
"Apps & Pages": "Apps & Pages",
"Email": "Email",
"Chat": "Chat",
"Invoice": "Invoice",
"Preview": "Preview",
"Add": "Add",
"User": "User",
"View": "View",
"Login v1": "Login v1",
"Login v2": "Login v2",
"Login": "Login",
"Register v1": "Register v1",
"Register v2": "Register v2",
"Register": "Register",
"Forget Password v1": "Forget Password v1",
"Forget Password v2": "Forget Password v2",
"Forgot Password v1": "Forgot Password v1",
"Forgot Password v2": "Forgot Password v2",
"Forgot Password": "Forgot Password",
"Reset Password v1": "Reset Password v1",
"Reset Password v2": "Reset Password v2",
"Reset Password": "Reset Password",
"Miscellaneous": "Miscellaneous",
"Coming Soon": "Coming Soon",
"Not Authorized": "Not Authorized",
"Under Maintenance": "Under Maintenance",
"Error": "Error",
"Errors": "Errors",
"Statistics": "Statistics",
"Actions": "Actions",
"Access Control": "Access Control",
"User Interface": "User Interface",
"CRM": "CRM",
"eCommerce": "eCommerce",
"Icons": "Icons",
"Chip": "Chip",
"Dialog": "Dialog",
"Expansion Panel": "Expansion Panel",
"Combobox": "Combobox",
"Textfield": "Textfield",
"Range Slider": "Range Slider",
"Menu": "Menu",
"Snackbar": "Snackbar",
"Tabs": "Tabs",
"Form Elements": "Form Elements",
"Form Layouts": "Form Layouts",
"Authentication": "Authentication",
"Page Not Found - 404": "Page Not Found - 404",
"Not Authorized - 401": "Not Authorized - 401",
"Server Error - 500": "Server Error - 500",
"2": "2",
"Forms": "Forms",
"Timeline": "Timeline",
"Disabled Menu": "Disabled Menu",
"Help Center": "Help Center",
"Verify Email": "Verify Email",
"Verify Email v1": "Verify Email v1",
"Verify Email v2": "Verify Email v2",
"Two Steps": "Two Steps",
"Two Steps v1": "Two Steps v1",
"Two Steps v2": "Two Steps v2",
"Custom Input": "Custom Input",
"Extensions": "Extensions",
"Tour": "Tour",
"Register Multi-Steps": "Register Multi-Steps",
"Wizard Examples": "Wizard Examples",
"Checkout": "Checkout",
"Create Deal": "Create Deal",
"Property Listing": "Property Listing",
"Roles & Permissions": "Roles & Permissions",
"Roles": "Roles",
"Simple Table": "Simple Table",
"Tables": "Tables",
"Data Table": "Data Table",
"Permissions": "Permissions",
"Apps": "Apps",
"Misc": "Misc",
"Wizard Pages": "Wizard Pages",
"Form Wizard": "Form Wizard",
"Numbered": "Numbered",
"3": "3",
"ecommerce": "ecommerce",
"Ecommerce": "Ecommerce",
"Editors": "Editors",
"Front Pages": "Front Pages",
"Landing": "Landing",
"checkout": "checkout",
"Payment": "Payment",
"Swiper": "Swiper",
"Product": "Product",
"Category": "Category",
"Order": "Order",
"Details": "Details",
"Customer": "Customer",
"Manage Review": "Manage Review",
"Referrals": "Referrals",
"Settings": "Settings",
"Overview": "Overview",
"My Course": "My Course",
"Course Details": "Course Details",
"Academy": "Academy",
"Logistics": "Logistics",
"Dashboard": "Dashboard",
"Fleet": "Fleet",
"5": "5",
"10": "10",
"20": "20",
"25": "25",
"50": "50",
"100": "100",
"$vuetify": {
"badge": "Badge",
"noDataText": "No data available",
"close": "Close",
"open": "open",
"carousel": {
"ariaLabel": {
"delimiter": "delimiter"
}
},
"dataFooter": {
"itemsPerPageText": "Items per page:",
"itemsPerPageAll": "All",
"pageText": "{0}-{1} of {2}",
"firstPage": "First Page",
"prevPage": "Previous Page",
"nextPage": "Next Page",
"lastPage": "Last Page"
},
"pagination": {
"ariaLabel": {
"root": "root",
"previous": "previous",
"next": "next",
"currentPage": "currentPage",
"page": "page"
}
},
"input": {
"clear": "clear",
"appendAction": "appendAction",
"prependAction": "prependAction",
"counterSize": "counterSize",
"otp": "otp"
},
"fileInput": {
"counterSize": "counterSize"
},
"rating": {
"ariaLabel": {
"item": "item"
}
}
}
}

283
src/plugins/i18n/locales/fr.json Executable file
View File

@ -0,0 +1,283 @@
{
"Home": "Accueil",
"Store": "Boutique",
"Stores": "Boutiques",
"Flow": "Flux",
"OBI": "OBI",
"Dotsoft": "Dotsoft",
"List of stores": "Liste des boutiques",
"Store visualization": "Visualisation boutique",
"BL not sent list": "Liste BL non envoyés",
"BL not sent": "BL non envoyés",
"ST ID": "ID ST",
"Exp ID": "ID Exp",
"BL ID": "ID BL",
"BL Date": "Date BL",
"External Code": "Code externe",
"Notes": "Notes",
"Expeditor": "Expéditeur",
"Distributor": "Distributeur",
"Distributors": "Distributeurs",
"Season": "Saison",
"Country": "Pays",
"Brand": "Marque",
"R-C-S": "R-C-T",
"Item ID": "ID produit",
"Chain": "Enseigne",
"Multi POS": "Multi POS",
"Search": "Chercher",
"Name": "Nom",
"Phone": "Téléphone",
"Mode": "Mode",
"Value": "Valeur",
"Item": "Article",
"Items": "Articles",
"Remote": "Accès distant",
"Option": "Option",
"Stock": "Stock",
"Price": "Prix",
"ITEM_ID": "ITEM ID",
"PRICE": "PRIX",
"TYPE": "TYPE",
"PARENT": "PARENT",
"LEVEL": "NIVEAU",
"Migration": "MIGRATION",
"EFFECTIVE DATE": "DATE EFFECTIVE",
"EXPIRATION DATE": "DATE EXPIRATION",
"DATE_CREATE": "DATE CREATION",
"USER_CREATE": "UTIL. CREATION",
"DATE_UPDATE": "DATE MAJ",
"USER_UPDATE": "UTIL. MAJ",
"Reload": "Rafraichir",
"WorkStation": "Caisse",
"SignatureString": "Chaine de Signature",
"SignatureSource": "Signature source",
"Submit": "Valider",
"You can search for a reference, reference-color or reference-color-size.": "Vous pouvez chercher une reference, reference-couleur ou reference-couleur-taille.",
"Minimum 5 characters long": "Minimum 5 caractères",
"Filters": "Filtres",
"XADMIN": "XADMIN",
"Logs": "Logs",
"Level": "Niveau",
"loggerCategory": "Categorie log",
"Thread": "Thread",
"Date": "Date",
"WSK": "Caisse",
"Select date": "Selectionner une date",
"ExcelData is empty, cannot export": "Aucune donnée, impossible d'exporter",
"Logs list": "Liste des logs",
"Business Date": "Date ouverture",
"Dates range error": "Dans le cas d'une plage de dates, un texte d'au moins 5 caractères doit être spécifié.",
"Close": "Fermer",
"Connect to invidual POS": "Connexion à une caisse individuelle",
"---------------------------": "---------------------------",
"UI Elements": "ÉLÉMENTS DE L'UI",
"Forms & Tables": "Formulaires et tableaux",
"Pages": "Des pages",
"Charts & Maps": "Graphiques et cartes",
"Others": "Autres",
"Typography": "Typographie",
"Cards": "Cartes",
"Basic": "De base",
"Advance": "Avance",
"Widgets": "Widget",
"Card Action": "Action de la carte",
"Components": "Composants",
"Alert": "Alerte",
"Close Alert": "Fermer l'alerte",
"Avatar": "Avatar",
"Badge": "Badge",
"Button": "Bouton",
"Calendar": "Calendrier",
"Image": "Image",
"Pagination": "Pagination",
"Progress Circular": "Progrès circulaire",
"Progress Linear": "Progrès Linéaire",
"Autocomplete": "Saisie automatique",
"Tooltip": "Info-bulle",
"Slider": "Glissière",
"Date Time Picker": "Sélecteur de date et d'heure",
"Select": "Sélectionner",
"Switch": "Commutateur",
"Checkbox": "Case à cocher",
"Radio": "Radio",
"Textarea": "Textarea",
"Rating": "Évaluation",
"File Input": "Entrée de fichier",
"Otp Input": "Entrée Otp",
"Form Layout": "Disposition du formulaire",
"Form Validation": "Validation de formulaire",
"Charts": "Graphiques",
"Apex Chart": "Graphique Apex",
"Chartjs": "Chartjs",
"Account Settings": "Paramètres du compte",
"User Profile": "Profil de l'utilisateur",
"FAQ": "FAQ",
"Dialog Examples": "Exemples de dialogue",
"Pricing": "Tarification",
"List": "liste",
"Edit": "Éditer",
"Nav Levels": "Niveaux de navigation",
"Level 2.1": "Niveau 2.1",
"Level 2.2": "Niveau 2.2",
"Level 3.1": "Niveau 3.1",
"Level 3.2": "Niveau 3.2",
"Raise Support": "Augmenter le soutien",
"Documentation": "Documentation",
"Dashboards": "Tableaux de bord",
"Analytics": "Analytique",
"Apps & Pages": "Applications et pages",
"Email": "Email",
"Chat": "Bavarder",
"Invoice": "Facture d'achat",
"Preview": "Aperçu",
"Add": "Ajouter",
"User": "Utilisateur",
"View": "Vue",
"Login v1": "Connexion v1",
"Login v2": "Connexion v2",
"Login": "Connexion",
"Register v1": "S'inscrire v1",
"Register v2": "S'inscrire v2",
"Register": "S'inscrire",
"Forget Password v1": "Oubliez le mot de passe v1",
"Forget Password v2": "Oubliez le mot de passe v2",
"Forgot Password v1": "Oubliez le mot de passe v1",
"Forgot Password v2": "Oubliez le mot de passe v2",
"Forgot Password": "Oubliez le mot de passe",
"Reset Password v1": "Réinitialiser le mot de passe v1",
"Reset Password v2": "Réinitialiser le mot de passe v2",
"Reset Password": "Réinitialiser le mot de passe",
"Miscellaneous": "Divers",
"Coming Soon": "Bientôt disponible",
"Not Authorized": "Pas autorisé",
"Under Maintenance": "En maintenance",
"Error": "Erreur",
"Errors": "Erreurs",
"Statistics": "Statistiques",
"Card Actions": "Actions de la carte",
"Actions": "Actions",
"Access Control": "Contrôle d'accès",
"User Interface": "Interface utilisateur",
"CRM": "CRM",
"eCommerce": "commerce électronique",
"Icons": "Icône",
"Chip": "Ébrécher",
"Dialog": "Dialogue",
"Expansion Panel": "Panneau d'extension",
"Combobox": "Boîte combo",
"Textfield": "Champ de texte",
"Range Slider": "Curseur Gamme",
"Menu": "Menu",
"Snackbar": "Casse-croûte",
"Tabs": "Onglets",
"Form Elements": "Éléments de formulaire",
"Form Layouts": "Dispositions de formulaire",
"Authentication": "Authentification",
"Page Not Found - 404": "Page introuvable - 404",
"Not Authorized - 401": "Non autorisé - 401",
"Server Error - 500": "Erreur de serveur - 500",
"2": "2",
"Forms": "Formes",
"Timeline": "Chronologie",
"Disabled Menu": "Menu désactivé",
"Help Center": "Centre d'aide",
"Verify Email": "Vérifier les courriels",
"Verify Email v1": "Vérifier l'e-mail v1",
"Verify Email v2": "Vérifier l'e-mail v2",
"Two Steps": "Deux étapes",
"Two Steps v1": "Deux étapes v1",
"Two Steps v2": "Deux étapes v2",
"Custom Input": "Entrée personnalisée",
"Extensions": "Rallonges",
"Tour": "Tour",
"Register Multi-Steps": "Enregistrer plusieurs étapes",
"Wizard Examples": "Exemples de guide",
"Checkout": "Check-out",
"Create Deal": "Créer une offre",
"Property Listing": "Liste des propriétés",
"Roles & Permissions": "Rôles et autorisations",
"Roles": "Rôles",
"Permissions": "Autorisations",
"Simple Table": "Table simple",
"Tables": "Tables",
"Data Table": "Table de données",
"Apps": "Applications",
"Misc": "Divers",
"Wizard Pages": "Pages de l'assistant",
"Form Wizard": "Assistant de formulaire",
"Numbered": "Numéroté",
"3": "3",
"ecommerce": "commerce électronique",
"Ecommerce": "Commerce électronique",
"Product": "Produit",
"Category": "Catégorie",
"Order": "Ordre",
"Details": "Détails",
"Customer": "Client",
"Manage Review": "Gérer la revue",
"Referrals": "Références",
"Settings": "Paramètres",
"Course Details": "Détails du cours",
"My Course": "Mon cours",
"Overview": "Aperçu",
"Academy": "Académie",
"Logistics": "Logistique",
"Dashboard": "Tableau de bord",
"Fleet": "Flotte",
"Editors": "Éditeurs",
"Front Pages": "Pages frontales",
"Landing": "d'atterrissage",
"checkout": "Check-out",
"Payment": "Paiement",
"Swiper": "Swiper",
"5": "5",
"10": "10",
"20": "20",
"25": "25",
"50": "50",
"100": "100",
"$vuetify": {
"badge": "Badge",
"noDataText": "Pas de données disponibles",
"close": "Fermer",
"open": "Ouvert",
"carousel": {
"ariaLabel": {
"delimiter": "délimiteur"
}
},
"dataFooter": {
"itemsPerPageText": "Objets par page:",
"itemsPerPageAll": "Tout",
"pageText": "{0}-{1} of {2}",
"firstPage": "Première page",
"prevPage": "Page précédente",
"nextPage": "Page suivante",
"lastPage": "Dernière page"
},
"pagination": {
"ariaLabel": {
"root": "racine",
"previous": "précédente",
"next": "suivante",
"currentPage": "page actuelle",
"page": "page"
}
},
"input": {
"clear": "dégager",
"appendAction": "ajouter une action",
"prependAction": "préfixer l'action",
"otp": "otp"
},
"fileInput": {
"counterSize": "Taille du compteur"
},
"rating": {
"ariaLabel": {
"item": "Objet"
}
}
}
}

17
src/plugins/i18n/vue-i18n.d.ts vendored Executable file
View File

@ -0,0 +1,17 @@
/**
* global type definitions
* using the typescript interface, you can define the i18n resources that is type-safed!
*/
/**
* you need to import the some interfaces
*/
import en from '@/plugins/i18n/locales/en.json';
import 'vue-i18n';
type LocaleMessage = typeof en
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends LocaleMessage {
}
}

View File

@ -0,0 +1,51 @@
import type { SortItem } from '@/@core/types'
export const useDataTableStore = defineStore({
id: 'dataTable',
state: () => ({
filters: {} as Record<string, string>,
currentPage: 1,
itemsPerPage: 10,
searchText: '',
sortBy: '',
sortOrder: false,
}),
actions: {
clearState() {
this.filters = {} as Record<string, string>
this.currentPage = 1
this.itemsPerPage = 10
this.searchText = ''
this.sortBy = ''
this.sortOrder = false
},
setFilters(filterName: string, value: any) {
this.filters[filterName] = value
},
setCurrentPage(page: number) {
this.currentPage = page
},
setItemsPerPage(value: number) {
this.itemsPerPage = value
},
setSearchText(value: string) {
this.searchText = value
},
setSortBy(value: string) {
this.sortBy = value
},
setSortOrder(value: string) {
if (value === 'asc')
this.sortOrder = true
else
this.sortOrder = false
},
getSortBy(): SortItem[] {
return [{
key: this.sortBy,
order: this.sortOrder ? 'asc' : 'desc',
}]
},
},
},
)

47
src/stores/user.store.ts Normal file
View File

@ -0,0 +1,47 @@
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(`${import.meta.env.VITE_API_BASE_URL}/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: any) {
console.log(err.message)
throw err // Rethrow the error so it can be caught in the component
}
},
logout() {
this.username = null
this.role = null
// Effacer les cookies
useCookie('accessToken').value = null
useCookie('userData').value = null
useCookie('userAbilityRules').value = null
},
},
})

View File

@ -1,13 +1,15 @@
import { ofetch } from 'ofetch'
export const $api = ofetch.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
baseURL: import.meta.env.VITE_API_BASE_URL,
async onRequest({ options }) {
const accessToken = useCookie('accessToken').value
if (accessToken) {
options.headers = {
...options.headers,
Authorization: `Bearer ${accessToken}`,
// 'Cache-Control': 'no-cache',
}
}
},

View File

@ -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>

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import type { StoreData } from '@/models/storeData'
import UserProfileHeaderBg from '@images/pages/user-profile-header-bg.png'
interface Props {
storeData: StoreData
}
const props = defineProps<Props>()
</script>
<template>
<VCard v-if="props">
<VImg
:src="UserProfileHeaderBg"
min-height="80"
max-height="120"
cover
/>
<VCardText class="d-flex align-bottom flex-sm-row flex-column justify-center gap-x-5">
<div class="d-flex h-0">
<VAvatar
rounded
size="120"
:image="props.storeData.store.photoLink"
class="user-profile-avatar mx-auto"
/>
</div>
<div class="user-profile-info w-100 mt-16 pt-6 pt-sm-0 mt-sm-0">
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4 mb-3">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-4">
<span class="d-flex">
<span class="text-h5">
{{ props.storeData.store.id_structure }} - {{ props.storeData.store.nom }}
</span>
</span>
</div>
<div class="d-flex align-center gap-4">
<div class="d-flex flex-column">
<span class="text-h6 font-weight-medium">{{ props.storeData.store.date_migration }}</span>
</div>
</div>
</div>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-4">
<span class="d-flex">
<VIcon
size="20"
icon="tabler-color-swatch"
class="me-1"
/>
<span class="text-body-1">
{{ props.storeData.store.enseigne }}
</span>
</span>
<span class="d-flex">
<VIcon
size="20"
icon="tabler-phone-call"
class="me-1"
/>
<span class="text-body-1">
{{ props.storeData.store.telephone }}
</span>
</span>
<span class="d-flex">
<VIcon
size="20"
icon="tabler-map-pin"
class="me-1"
/>
<span class="text-body-1">
{{ props.storeData.store.adresse }} - ({{ props.storeData.store.pays }})
</span>
</span>
</div>
<div class="d-flex align-center gap-4">
<VAvatar
color="primary"
variant="tonal"
size="42"
>
<VIcon icon="tabler-http-post" />
</VAvatar>
<div class="d-flex flex-column">
<span class="text-h5 font-weight-medium">{{ props.storeData.store.nbcaisses }}</span>
<span class="text-sm">
Caisses
</span>
</div>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>
<style lang="scss">
.user-profile-avatar {
border: 5px solid rgb(var(--v-theme-surface));
background-color: rgb(var(--v-theme-surface)) !important;
inset-block-start: -3rem;
.v-img__img {
border-radius: 0.125rem;
}
}
</style>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { StoreData } from '@/models/storeData'
import StoreTabAdminH from '@/views/pages/store/view/StoreTabAdminH.vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<Props>()
interface Props {
storeData: StoreData
}
</script>
<template>
<div>
<VRow>
<VCol cols="12">
<StoreTabAdminH :store-data="storeData" />
</VCol>
</VRow>
</div>
</template>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import type { StoreData } from '@/models/storeData'
import StoreTabAdminHsequence from '@/views/pages/store/view/StoreTabAdminHsequence.vue'
import StoreTabAdminHsignature from '@/views/pages/store/view/StoreTabAdminHsignature.vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<Props>()
interface Props {
storeData: StoreData
}
const currentTab = ref('tab-1')
</script>
<template>
<VCard>
<VTabs
v-model="currentTab"
grow
stacked
>
<VTab>
<VIcon
start
icon="tabler-123"
/>
Sequence
</VTab>
<VTab>
<VIcon
start
icon="tabler-certificate"
/>
Signature
</VTab>
</VTabs>
<VCardText style="padding: 5px !important;">
<VWindow
v-model="currentTab"
class="ms-3"
>
<VWindowItem value="tab-1">
<StoreTabAdminHsequence :store-data="storeData" />
</VWindowItem>
<VWindowItem value="tab-2">
<StoreTabAdminHsignature :store-data="storeData" />
</VWindowItem>
</VWindow>
</VCardText>
</VCard>
</template>

View File

@ -0,0 +1,171 @@
<script setup lang="ts">
import { VDataTable } from 'vuetify/labs/VDataTable'
import type { StoreData } from '@/models/storeData'
const props = defineProps<Props>()
const { t } = useI18n()
const route = useRoute('store-details')
const headers = computed(() => [
{ title: 'WSK', key: 'wkstnId' },
{ title: t('Name'), key: 'sequenceId' },
{ title: t('Mode'), key: 'sequenceMode' },
{ title: t('Value'), key: 'sequenceNbr' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
const selectedWkStn = ref()
const selectedSequenceMode = ref()
const searchQuery = ref('')
interface Props {
storeData: StoreData
}
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const isLoading = ref(false)
const data = ref([]) // Initialisez data comme un tableau vide
const fetchData = async () => {
isLoading.value = true
const response = await useApi<any>(createUrl(`/stores/${props.storeData.store.id_structure}/sequence?dbHost=${route.query.dbHost}`))
data.value = response.data.value
isLoading.value = false
}
const wkstn = computed(() => {
const allWkstns = data.value.map((store: { wkstnId: any }) => store.wkstnId)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueWkstns = allWkstns.filter((wkstn: any, index: any, self: string | any[]) => self.indexOf(wkstn) === index)
const sortedWkstns = uniqueWkstns.sort()
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedWkstns.map((wkstn: any) => ({ title: wkstn, value: wkstn }))
})
const seqmode = computed(() => {
const allSeqModes = data.value.map((store: { sequenceMode: any }) => store.sequenceMode)
const uniqueSeqModes = allSeqModes.filter((seqmod: any, index: any, self: string | any[]) => self.indexOf(seqmod) === index)
const sortedSeqModes = uniqueSeqModes.sort()
return sortedSeqModes.map((seqmod: any) => ({ title: seqmod, value: seqmod }))
})
const filteredSequenceList = computed(() => {
let filtered = data.value
// If a workstation is selected, filter the records for this number
if (selectedWkStn.value !== undefined && selectedWkStn.value !== null)
filtered = filtered.filter((store: { wkstnId: any }) => store.wkstnId === selectedWkStn.value)
// If a sequence mode is selected, filter the records for this mode
if (selectedSequenceMode.value !== undefined && selectedSequenceMode.value !== null)
filtered = filtered.filter((store: { sequenceMode: any }) => store.sequenceMode === selectedSequenceMode.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((store: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(store).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
</script>
<template>
<div>
<!-- 👉 stores -->
<VCard class="mb-6">
<VCardText>
<VRow>
<!-- 👉 Select country -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedWkStn"
:placeholder="$t('WorkStation')"
:items="wkstn"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Brand -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedSequenceMode"
:placeholder="$t('Mode')"
:items="seqmode"
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="$t('Search')"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-4 flex-wrap align-center">
<VBtn
color="primary"
prepend-icon="tabler-reload"
@click="fetchData"
>
{{ $t('Reload') }}
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable -->
<VDataTable
v-else
:headers="headers"
:items="filteredSequenceList"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</VCard>
</div>
</template>

View File

@ -0,0 +1,172 @@
<script setup lang="ts">
import { VDataTable } from 'vuetify/labs/VDataTable'
import type { StoreData } from '@/models/storeData'
const props = defineProps<Props>()
const { t } = useI18n()
const route = useRoute('store-details')
const headers = computed(() => [
{ title: 'WSK', key: 'wkstnId' },
{ title: t('Name'), key: 'signatureId' },
{ title: t('Mode'), key: 'signatureMode' },
{ title: t('SignatureString'), key: 'signatureString' },
{ title: t('SignatureSource'), key: 'signatureSource' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
const selectedWkStn = ref()
const selectedSignatureMode = ref()
const searchQuery = ref('')
interface Props {
storeData: StoreData
}
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const isLoading = ref(false)
const data = ref([]) // Initialisez data comme un tableau vide
const fetchData = async () => {
isLoading.value = true
const response = await useApi<any>(createUrl(`/stores/${props.storeData.store.id_structure}/signature?dbHost=${route.query.dbHost}`))
data.value = response.data.value
isLoading.value = false
}
const wkstn = computed(() => {
const allWkstns = data.value.map((store: { wkstnId: any }) => store.wkstnId)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueWkstns = allWkstns.filter((wkstn: any, index: any, self: string | any[]) => self.indexOf(wkstn) === index)
const sortedWkstns = uniqueWkstns.sort()
// eslint-disable-next-line @typescript-eslint/no-shadow
return sortedWkstns.map((wkstn: any) => ({ title: wkstn, value: wkstn }))
})
const signmode = computed(() => {
const allSignModes = data.value.map((store: { signMode: any }) => store.signMode)
const uniqueSignMoeds = allSignModes.filter((signmod: any, index: any, self: string | any[]) => self.indexOf(signmod) === index)
const sortedSignMoeds = uniqueSignMoeds.sort()
return sortedSignMoeds.map((signmod: any) => ({ title: signmod, value: signmod }))
})
const filteredSigantureList = computed(() => {
let filtered = data.value
// If a workstation is selected, filter the records for this number
if (selectedWkStn.value !== undefined && selectedWkStn.value !== null)
filtered = filtered.filter((store: { wkstnId: any }) => store.wkstnId === selectedWkStn.value)
// If a signature mode is selected, filter the records for this mode
if (selectedSignatureMode.value !== undefined && selectedSignatureMode.value !== null)
filtered = filtered.filter((store: { signMode: any }) => store.signMode === selectedSignatureMode.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((store: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(store).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
</script>
<template>
<div>
<!-- 👉 stores -->
<VCard class="mb-6">
<VCardText>
<VRow>
<!-- 👉 Select country -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedWkStn"
:placeholder="$t('WorkStation')"
:items="wkstn"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Brand -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedSignatureMode"
:placeholder="$t('Mode')"
:items="signmode"
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="$t('Search')"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-4 flex-wrap align-center">
<VBtn
color="primary"
prepend-icon="tabler-reload"
@click="fetchData"
>
{{ $t('Reload') }}
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable -->
<VDataTable
v-else
:headers="headers"
:items="filteredSigantureList"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</VCard>
</div>
</template>

View File

@ -0,0 +1,231 @@
<script setup lang="ts">
import type { StoreData } from '@/models/storeData'
interface Props {
storeData: StoreData
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<Props>()
</script>
<template>
<div
v-for="(pos, index) in storeData.pos"
:key="index"
>
<VChip
label
size="small"
class="text-capitalize me-1"
>
Pos {{ pos.workstationId }}
</VChip>
<VChip
label
size="small"
class="text-capitalize"
>
{{ pos.ip }}
</VChip>
/
<VChip
label
size="small"
class="text-capitalize me-3"
color="primary"
>
{{ pos.primaryRegister ? 'Primary' : 'Slave' }}
</VChip>
<VChip
v-if="pos.fatalError"
label
size="small"
class="text-capitalize"
color="error"
>
Fatal
</VChip>
<VRow class="py-3 mb-1">
<!-- 👉 Business date & opening hours -->
<VCol
cols="12"
md="3"
:class="$vuetify.display.mdAndUp ? 'border-e' : 'border-b'"
>
<div class="ape-3">
<div class="d-flex justify-space-between flex-wrap gap-4 flex-column flex-xs-row">
<div>
<div class="d-flex mb-4">
<VAvatar
variant="tonal"
color="primary"
rounded
size="54"
class="text-primary me-4"
>
<VIcon
icon="tabler-calendar-event"
size="38"
/>
</VAvatar>
<div>
<span class="text-base">Business date</span>
<h4 class="text-h4 font-weight-medium text-primary">
{{ pos.businessDateS }}
</h4>
</div>
</div>
<div class="d-flex">
<VAvatar
variant="tonal"
color="info"
rounded
size="54"
class="text-primary me-4"
>
<VIcon
icon="tabler-clock-hour-10"
size="38"
/>
</VAvatar>
<div>
<span class="text-base">Opening hours</span>
<h4 class="text-h6 font-weight-medium text-info">
{{ pos.openingDate }}{{ pos.closingDate ? ` ${pos.closingDate}` : '' }}
</h4>
</div>
</div>
</div>
</div>
</div>
</VCol>
<!-- 👉 Pending Replication -->
<VCol
cols="12"
md="3"
:class="$vuetify.display.mdAndUp ? 'border-e' : 'border-b'"
>
<div class="d-flex justify-space-between align-center">
<div class="d-flex flex-column ps-3">
<h5 class="text-h5 text-high-emphasis mb-0 text-no-wrap">
Pending replication
</h5>
<div class="text-h3 mb-2">
{{ pos.replication.pendingReplications }}
</div>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Min
</VChip>
{{ pos.replication.minPendingReplicationDate }}
</div>
</span>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Max
</VChip>
{{ pos.replication.maxPendingReplicationDate }}
</div>
</span>
</div>
</div>
</VCol>
<!-- 👉 XSTORE Ticket -->
<VCol
cols="12"
md="3"
:class="$vuetify.display.mdAndUp ? 'border-e' : 'border-b'"
>
<div class="d-flex justify-space-between align-center">
<div class="d-flex flex-column ps-3">
<h5 class="text-h5 text-high-emphasis mb-0 text-no-wrap">
XSTORE Tickets
</h5>
<div class="text-h3 mb-2">
{{ pos.saleTransaction.count }}
</div>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Min
</VChip>
{{ pos.saleTransaction.minDate }}
</div>
</span>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Max
</VChip>
{{ pos.saleTransaction.maxDate }}
</div>
</span>
</div>
</div>
</VCol>
<!-- 👉 DOTSOFT Tickets -->
<VCol
cols="12"
md="3"
>
<div class="d-flex justify-space-between align-center">
<div class="d-flex flex-column ps-3">
<h5 class="text-h5 text-high-emphasis mb-0 text-no-wrap">
DOTSOFT Tickets
</h5>
<div class="text-h3 mb-2">
{{ pos.boTransaction.backOfficeTransactions }}
</div>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Min
</VChip>
{{ pos.boTransaction.minBackOfficeTransactionDate }}
</div>
</span>
<span class="mb-1">
<div>
<VChip
variant="tonal"
color="secondary"
style="inline-size:45px"
>
Max
</VChip>
{{ pos.boTransaction.maxBackOfficeTransactionDate }}
</div>
</span>
</div>
</div>
</VCol>
</VRow>
</div>
</template>

View File

@ -0,0 +1,84 @@
<script lang="ts" setup>
import { provide, ref } from 'vue'
import { VForm } from 'vuetify/components/VForm'
import StoreTabItemH from '@/views/pages/store/view/StoreTabItemH.vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { t } = useI18n()
const itemForm = ref('')
const injItemForm = ref('')
const refItemForm = ref<VForm>()
provide('item', injItemForm)
const validateItemForm = () => {
refItemForm.value?.validate().then(valid => {
if (valid.valid) {
console.log(itemForm.value)
injItemForm.value = itemForm.value
}
else { console.log(`KO:${itemForm.value}`) }
})
}
const lengthMinValidator = (value: unknown, length: number) => {
if (isEmpty(value))
return true
return String(value).length >= length || `The Min Character field must be at least ${length} characters`
}
</script>
<template>
<div>
<VRow>
<VCol cols="12">
<!-- 👉 Item search -->
<VCard title="">
<VCardText>
<VAlert
variant="tonal"
color="warning"
class="mb-4"
>
<VAlertTitle class="mb-2">
{{ $t('You can search for a reference, reference-color or reference-color-size.') }}
</VAlertTitle>
<span>{{ $t('Minimum 5 characters long') }}</span>
</VAlert>
<VForm
ref="refItemForm"
@submit.prevent="validateItemForm"
>
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="itemForm"
label="Reference"
persistent-placeholder
placeholder="QY10010"
:rules="[requiredValidator, lengthMinValidator(itemForm, 5)]"
/>
</VCol>
<VCol cols="12">
<VBtn type="submit">
{{ $t('Submit') }}
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCol cols="12">
<StoreTabItemH />
</VCol>
</VCard>
</VCol>
</VRow>
</div>
</template>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { inject } from 'vue'
import StoreTabItemHitem from '@/views/pages/store/view/StoreTabItemHitem.vue'
import StoreTabItemHoption from '@/views/pages/store/view/StoreTabItemHoption.vue'
import StoreTabItemHprice from '@/views/pages/store/view/StoreTabItemHprice.vue'
import StoreTabItemHstock from '@/views/pages/store/view/StoreTabItemHstock.vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { t } = useI18n()
const item = inject<string>('item')
const currentTab = ref('tab-1')
</script>
<template>
<VCard>
<VTabs
v-model="currentTab"
grow
stacked
>
<VTab>
<VIcon
start
icon="tabler-shirt-sport"
/>
{{ $t('Item') }}
</VTab>
<VTab>
<VIcon
start
icon="tabler-report-money"
/>
{{ $t('Option') }}
</VTab>
<VTab>
<VIcon
start
icon="tabler-currency-euro"
/>
{{ $t('Price') }}
</VTab>
<VTab>
<VIcon
start
icon="tabler-building-warehouse"
/>
{{ $t('Stock') }}
</VTab>
</VTabs>
<VCardText style="padding: 5px !important;">
<VWindow
v-model="currentTab"
class="ms-3"
>
<VWindowItem value="tab-1">
<StoreTabItemHitem :item="item" />
</VWindowItem>
<VWindowItem value="tab-2">
<StoreTabItemHoption :item="item" />
</VWindowItem>
<VWindowItem value="tab-3">
<StoreTabItemHprice :item="item" />
</VWindowItem>
<VWindowItem value="tab-4">
<StoreTabItemHstock :item="item" />
</VWindowItem>
</VWindow>
</VCardText>
</VCard>
</template>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { watchEffect } from 'vue'
import { VDataTable } from 'vuetify/labs/VDataTable'
const props = defineProps({
item: String,
})
const { t } = useI18n()
const route = useRoute('store-details')
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const fetchedData = ref<Array<Record<string, unknown>>>([])
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
const { data } = await useApi<any>(createUrl(`/items/${props.item}?dbHost=${route.query.dbHost}`))
fetchedData.value = data.value
isLoading.value = false
}
watchEffect(() => {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (props.item)
// eslint-disable-next-line curly
if (!(props.item.trim() === ''))
fetchData()
})
// const { t } = useI18n()
const headers = computed(() => [
{ title: t('ITEM_ID'), key: 'itemId' },
{ title: t('LEVEL'), key: 'itemLevelCode' },
{ title: t('PARENT'), key: 'parentItemId' },
{ title: t('TYPE'), key: 'itemTypeCode' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
</script>
<template>
<div>
<VCol>
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<VDataTable
v-else
:headers="headers"
:items="fetchedData"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { watchEffect } from 'vue'
import { VDataTable } from 'vuetify/labs/VDataTable'
const props = defineProps({
item: String,
})
const { t } = useI18n()
const route = useRoute('store-details')
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const isLoading = ref(false)
const fetchedData = ref<Array<Record<string, unknown>>>([])
const fetchData = async () => {
isLoading.value = true
const { data } = await useApi<any>(createUrl(`/items/${props.item}/options?dbHost=${route.query.dbHost}`))
fetchedData.value = data.value.map((item: any) => ({
...item,
level: `${item.levelCode}:${item.levelValue}`,
}))
isLoading.value = false
}
watchEffect(() => {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (props.item)
// eslint-disable-next-line curly
if (!(props.item.trim() === ''))
fetchData()
})
// const { t } = useI18n()
const headers = computed(() => [
{ title: t('ITEM_ID'), key: 'itemId' },
{ title: 'LEVEL', key: 'level' },
{ title: 'VENDABLE', key: 'itemAvailabilityCode' },
{ title: 'TAXE', key: 'taxGroupId' },
{ title: 'VENDOR', key: 'vendor' },
{ title: 'SEASON', key: 'seasonCode' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
</script>
<template>
<div>
<VCol>
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable -->
<VDataTable
v-else
:headers="headers"
:items="fetchedData"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { watchEffect } from 'vue'
import { VDataTable } from 'vuetify/labs/VDataTable'
const props = defineProps({
item: String,
})
const { t } = useI18n()
const route = useRoute('store-details')
const isLoading = ref(false)
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const fetchedData = ref<Array<Record<string, unknown>>>([])
const fetchData = async () => {
isLoading.value = true
const { data } = await useApi<any>(createUrl(`/items/${props.item}/price?dbHost=${route.query.dbHost}`))
fetchedData.value = data.value.map((item: any) => ({
...item,
level: `${item.levelCode}:${item.levelValue}`,
}))
isLoading.value = false
}
watchEffect(() => {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (props.item)
// eslint-disable-next-line curly
if (!(props.item.trim() === ''))
fetchData()
})
// const { t } = useI18n()
const headers = computed(() => [
{ title: t('ITEM_ID'), key: 'itemId' },
{ title: t('LEVEL'), key: 'level' },
{ title: t('TYPE'), key: 'itmPricePropertyCode' },
{ title: t('EFFECTIVE DATE'), key: 'effectiveDate' },
{ title: t('EXPIRATION DATE'), key: 'expirationDate' },
{ title: t('PRICE'), key: 'price' },
{ title: 'EXTERNAL ID', key: 'externalId' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
</script>
<template>
<div>
<VCol>
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<VDataTable
v-else
:headers="headers"
:items="fetchedData"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</div>
</template>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { watchEffect } from 'vue'
import { VDataTable } from 'vuetify/labs/VDataTable'
const props = defineProps({
item: String,
})
const { t } = useI18n()
const route = useRoute('store-details')
// Data table options
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [''], sortDesc: [false] })
const fetchedData = ref<Array<Record<string, unknown>>>([])
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
const { data } = await useApi<any>(createUrl(`/items/${props.item}/stock?dbHost=${route.query.dbHost}`))
fetchedData.value = data.value
isLoading.value = false
}
watchEffect(() => {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (props.item)
// eslint-disable-next-line curly
if (!(props.item.trim() === ''))
fetchData()
})
// const { t } = useI18n()
const headers = computed(() => [
{ title: t('ITEM_ID'), key: 'itemId' },
{ title: 'LOCATION', key: 'invLocationId' },
{ title: 'BUCKET', key: 'bucketId' },
{ title: 'STOCK', key: 'unitCount' },
{ title: t('DATE_CREATE'), key: 'createDate' },
{ title: t('USER_CREATE'), key: 'createUserId' },
{ title: t('DATE_UPDATE'), key: 'updateDate' },
{ title: t('USER_UPDATE'), key: 'updateUserId' },
])
</script>
<template>
<div>
<VCol>
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<VDataTable
v-else
:headers="headers"
:items="fetchedData"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
/>
</VCol>
</div>
</template>

View File

@ -0,0 +1,325 @@
<script setup lang="ts">
import { endOfDay, format } from 'date-fns'
import exportFromJSON from 'export-from-json'
import { VDataTable } from 'vuetify/labs/VDataTable'
import type { StoreData } from '@/models/storeData'
const props = defineProps<Props>()
const { t } = useI18n()
const route = useRoute('store-details')
const headers = computed(() => [
{ title: '', key: 'data-table-expand' },
{ title: t('WSK'), key: 'wkstnId' },
{ title: t('Level'), key: 'logLevel' },
{ title: t('Category'), key: 'loggerCategory' },
{ title: t('Thread'), key: 'threadName' },
{ title: t('Date'), key: 'createDate' },
{ title: t('Business Date'), key: 'businessDate' },
{ title: t('User'), key: 'createUserId' },
])
const selectedWkStn = ref(null)
const selectedLogLevel = ref(null)
const selectedCategory = ref(null)
const selectedDate = ref(null)
const searchQuery = ref('')
interface Props {
storeData: StoreData
}
const isSnackbarExport = ref(false)
// Data table options
// TODO SortBy ne marche pas
const options = ref({ page: 1, itemsPerPage: 10, sortBy: ['createDate'], sortDesc: [true] })
const isLoading = ref(false)
const dtListData = ref([])
const fetchData = async () => {
isLoading.value = true
isSnackbarExport.value = false
let logDate
if (selectedDate.value)
logDate = format(new Date(selectedDate.value), 'yyyyMMdd')
else
logDate = format(endOfDay(new Date()), 'yyyyMMdd')
const response = await useApi<any>(createUrl(`/stores/${props.storeData.store.id_structure}/log?dbHost=${route.query.dbHost}&logDate=${logDate}`))
if (response.data.value)
dtListData.value = response.data.value
else
dtListData.value = []
isLoading.value = false
}
const wkstn = computed(() => {
const allWkstns = dtListData.value.map((list: { wkstnId: any }) => list.wkstnId)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueWkstns = allWkstns.filter((wkstn: any, index: any, self: string | any[]) => self.indexOf(wkstn) === index)
const sortedWkstns = uniqueWkstns.sort()
return sortedWkstns.map((list: any) => ({ title: list, value: list }))
})
const level = computed(() => {
const allLogLevels = dtListData.value.map((list: { logLevel: any }) => list.logLevel)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueLogLevels = allLogLevels.filter((level: any, index: any, self: string | any[]) => self.indexOf(level) === index)
const sortedLogLevels = uniqueLogLevels.sort()
return sortedLogLevels.map((list: any) => ({ title: list, value: list }))
})
const category = computed(() => {
const allCategories = dtListData.value.map((list: { loggerCategory: any }) => list.loggerCategory)
// eslint-disable-next-line @typescript-eslint/no-shadow
const uniqueCategories = allCategories.filter((category: any, index: any, self: string | any[]) => self.indexOf(category) === index)
const sortedCategories = uniqueCategories.sort()
return sortedCategories.map((list: any) => ({ title: list, value: list }))
})
const filteredLogList = computed(() => {
let filtered = dtListData.value
// If a workstation is selected, filter the records for this number
if (selectedWkStn.value !== undefined && selectedWkStn.value !== null)
filtered = filtered.filter((list: { wkstnId: any }) => list.wkstnId === selectedWkStn.value)
// If a loglevel is selected, filter the records for this Loglevel
if (selectedLogLevel.value !== undefined && selectedLogLevel.value !== null)
filtered = filtered.filter((list: { logLevel: any }) => list.logLevel === selectedLogLevel.value)
// If a loggerCategory is selected, filter the records for this loggerCategory
if (selectedCategory.value !== undefined && selectedCategory.value !== null)
filtered = filtered.filter((list: { loggerCategory: any }) => list.loggerCategory === selectedCategory.value)
// If a search query is provided, filter the records for this query
if (searchQuery.value) {
filtered = filtered.filter((log: { [s: string]: unknown } | ArrayLike<unknown>) =>
Object.values(log).some(value =>
String(value).toLowerCase().includes(searchQuery.value.toLowerCase()),
),
)
}
return filtered
})
const ExcelField = [
'wkstnId',
'logLevel',
'threadName',
'logMessage',
'loggerCategory',
'createDate',
'createUserId',
]
const ExcelData = computed(() => {
return dtListData.value.map((item: { [x: string]: string }) => {
const orderedItem: { [key: string]: string } = {}
ExcelField.forEach(field => {
if (field === 'createDate') {
const date = new Date(item[field])
orderedItem[field] = format(date, 'dd/MM/yyyy HH:mm:ss')
}
else {
orderedItem[field] = item[field]
}
})
return orderedItem
})
})
const exportEXCEL = () => {
const fileName = `LOGS_${props.storeData.store.id_structure}-${format(new Date(), 'yyyyMMdd')}`
if (ExcelData.value.length > 0) {
exportFromJSON({
data: ExcelData.value,
fileName,
exportType: exportFromJSON.types.xls,
})
}
else {
isSnackbarExport.value = true
}
}
</script>
<template>
<div>
<!-- 👉 filters -->
<VCard
:title="$t('Filters')"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select workstation -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedWkStn"
:placeholder="$t('WorkStation')"
:items="wkstn"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select log level -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedLogLevel"
:placeholder="$t('Level')"
:items="level"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select logger category -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedCategory"
:placeholder="$t('loggerCategory')"
:items="category"
clearable
clear-icon="tabler-x"
/>
</VCol>
</VRow>
<VRow>
<!-- 👉 Date log -->
<VCol sm="4">
<AppDateTimePicker
v-model="selectedDate"
label=""
:placeholder="$t('Select date')"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- 👉 datatable, search & export & reload -->
<VCard
:title="$t('Logs list')"
class="mb-6"
>
<div class="d-flex flex-wrap gap-4 mx-5">
<div class="d-flex align-center">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
:placeholder="$t('Search')"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-1 flex-wrap aalign-center">
<!-- 👉 Export button -->
<VBtn
variant="tonal"
color="primary"
prepend-icon="tabler-upload"
@click="exportEXCEL"
>
Export
</VBtn>
</div>
<!-- 👉 Reload -->
<div class="d-flex gap-4 flex-wrap align-center">
<VBtn
color="primary"
prepend-icon="tabler-reload"
@click="fetchData"
>
{{ $t('Reload') }}
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<VCol cols="12">
<div v-if="isLoading">
<VProgressCircular
indeterminate
color="primary"
/>
</div>
<!-- 👉 Datatable -->
<VDataTable
v-else
class="dt-row-striped"
:headers="headers"
:items="filteredLogList"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
density="compact"
expand-on-click
>
<!-- format date log -->
<template #item.createDate="{ item }">
{{ format(new Date(item.createDate), 'dd/MM/yyyy HH:mm:ss') }}
</template>
<!-- format business date -->
<template #item.businessDate="{ item }">
{{ format(new Date(item.businessDate), 'dd/MM/yyyy') }}
</template>
<!-- Expanded Row Data -->
<template #expanded-row="slotProps">
<tr class="v-data-table__tr">
<td :colspan="headers.length">
<p class="my-1">
{{ slotProps.item.logMessage }}
</p>
</td>
</tr>
</template>
</VDataTable>
</VCol>
</VCard>
</div>
<!-- Snackbar Export -->
<VSnackbar
v-model="isSnackbarExport"
location="center"
>
{{ $t('ExcelData is empty, cannot export') }}
<template #actions>
<VBtn
color="error"
@click="isSnackbarExport = false"
>
{{ $t("Close") }}
</VBtn>
</template>
</VSnackbar>
</template>

View File

@ -0,0 +1,136 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { StoreData } from '@/models/storeData'
import logoVNC from '@images/misc/remote_128.png'
const props = defineProps<Props>()
const drive = ['M:', 'N:', 'O:', 'P:', 'Q:', 'R:', 'S:', 'T:']
const selectedProtocol = ref('hdpos')
interface Props {
storeData: StoreData
}
const localStoreData = ref()
onMounted(() => {
// use copy a store data to prevent changes of ip field
localStoreData.value = JSON.parse(JSON.stringify(props.storeData))
localStoreData.value.store.caisses = localStoreData.value.store.caisses.map((caisse: any) => ({
...caisse,
drive: null, // replace null with the initial value you want to use
}))
})
const openVnc = (ip: string) => {
let url = ''
if (selectedProtocol.value === 'hdpos')
url = `hdpos://tightvnc?ip=${ip}`
else
url = `${selectedProtocol.value}://${ip}`
window.open(url, '_blank')
}
// eslint-disable-next-line @typescript-eslint/no-shadow
const mountDrive = (ip: string, drive: string) => {
const url = `hdpos://monter?lettre=${drive}&ip=${ip}`
window.open(url, '_blank')
}
</script>
<template>
<VRow>
<VCol
cols="12"
md="6"
>
<VCard
flat
border
>
<VCardText>
<VCardText class="text-center d-flex align-center justify-left">
<img
:src="logoVNC"
size="128"
>
<AppSelect
v-model="selectedProtocol"
:items="['hdpos', 'vnc']"
class="me-3"
style="max-inline-size: 130px"
/>
</VCardText>
<div v-if="localStoreData && localStoreData.store.caisses">
<div
v-for="(caisse, index) in localStoreData.store.caisses"
:key="index"
class="d-flex align-items-center mb-5"
>
<VRow>
<VCol
cols="6"
md="6"
class="d-flex align-items-center"
>
<VTextField
v-model.lazy="caisse.ip"
label="IP"
class="me-3"
/>
<VBtn
v-if="caisse.ip"
color="primary"
@click="openVnc(caisse.ip)"
>
Pos {{ caisse.id_caisse }}
</VBtn>
<VBtn
v-else
disabled
color="primary"
>
Pos {{ caisse.id_caisse }}
</VBtn>
</VCol>
<VCol
cols="12"
md="6"
class="d-flex align-items-center"
>
<AppSelect
v-model="caisse.drive"
:items="drive"
label=""
density="compact"
placeholder="Select"
class="me-3"
/>
<VBtn
v-if="caisse.drive"
color="primary"
@click="mountDrive(caisse.ip, caisse.drive)"
>
Mount drive {{ caisse.drive }}
</VBtn>
<VBtn
v-else
disabled
color="primary"
>
Mount drive
</VBtn>
</VCol>
</VRow>
</div>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@ -10,14 +10,14 @@ 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,
defaultLocale: 'en',
enable: true,
defaultLocale: 'fr',
langConfig: [
{
label: 'English',

View File

@ -45,6 +45,9 @@
],
"@api-utils/*": [
"./src/plugins/fake-api/utils/*"
],
"@stores/*": [
"./src/stores/*"
]
},
"lib": [

5
typed-router.d.ts vendored
View File

@ -41,8 +41,11 @@ declare module 'vue-router/auto/routes' {
export interface RouteNamedMap {
'root': RouteRecordInfo<'root', '/', Record<never, never>, Record<never, never>>,
'$error': RouteRecordInfo<'$error', '/:error(.*)', { error: ParamValue<true> }, { error: ParamValue<false> }>,
'flux-bl-not-sent': RouteRecordInfo<'flux-bl-not-sent', '/flux/bl/not_sent', Record<never, never>, Record<never, never>>,
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
'second-page': RouteRecordInfo<'second-page', '/second-page', Record<never, never>, Record<never, never>>,
'store-details': RouteRecordInfo<'store-details', '/store/details', Record<never, never>, Record<never, never>>,
'store-list': RouteRecordInfo<'store-list', '/store/list', Record<never, never>, Record<never, never>>,
'xadmin-log': RouteRecordInfo<'xadmin-log', '/xadmin/log', Record<never, never>, Record<never, never>>,
}
}

View File

@ -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: {