jws-frontend/src/layouts/DrawerComponent.vue
Methapon2001 21699b14c5
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
feat: troubleshooting page
2025-04-25 15:15:37 +07:00

624 lines
16 KiB
Vue

<script setup lang="ts">
import { computed, ref, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue';
import useMyBranch from 'stores/my-branch';
import { getUserId, getRole } from 'src/services/keycloak';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { isRoleInclude } from 'src/stores/utils';
type Menu = {
label: string;
route?: string;
icon?: string;
hidden?: boolean;
disabled?: boolean;
isax?: boolean;
noI18n?: boolean;
children?: Menu[];
};
const router = useRouter();
const $q = useQuasar();
const { locale } = useI18n();
const userBranch = useMyBranch();
const { currentMyBranch } = storeToRefs(userBranch);
const leftDrawerOpen = defineModel<boolean>('leftDrawerOpen', {
default: true,
});
const props = defineProps<{
mini?: boolean;
}>();
const role = ref();
const menuActive = ref<boolean[]>([]);
const menuData = ref<Menu[]>([]);
const currentPath = computed(() => {
return router.currentRoute.value.path;
});
function navigateTo(label: string, destination?: string) {
if (!destination) return;
router.push(`${destination}`);
}
function reActiveMenu() {
const currMenu = menuData.value.find((m) =>
m.children?.some((c) => c.route === currentPath.value),
);
const currMenuIndex = menuData.value.findIndex((m) => m === currMenu);
if ($q.screen.lt.sm) menuActive.value.fill(false);
menuActive.value[currMenuIndex] = true;
}
function branchSetting() {
//TODO: click setting (cog icon) on drawer menu
}
function initMenu() {
menuData.value = [
{
label: 'menu.manage',
icon: 'mdi-account-cog-outline',
children: [
{
label: 'branch',
route: '/branch-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
]),
},
{
label: 'personnel',
route: '/personnel-management',
hidden: !isRoleInclude([
'owner',
'system',
'head_of_admin',
'admin',
'branch_manager',
]),
},
{
label: 'workflow',
route: '/workflow',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
},
{
label: 'property',
route: '/property',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
},
{
label: 'productService',
route: '/product-service',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'head_of_sale',
'sale',
]),
},
{
label: 'customer',
route: '/customer-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
},
{
label: 'agencies',
route: '/agencies-management',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
},
],
},
{
label: 'menu.sales',
icon: 'mdi-store-settings-outline',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'quotation', route: '/quotation' },
{ label: 'invoice', route: '/invoice' },
],
},
{
label: 'menu.order',
icon: 'mdi-file-chart-outline',
disabled: false,
children: [
{ label: 'requestList', route: '/request-list' },
{ label: 'documentCheck', route: '', hidden: true },
{ label: 'taskOrder', route: '/task-order' },
{ label: 'goodReceipt', route: '', hidden: true },
{ label: 'workReceipt', route: '', hidden: true },
{ label: 'workDelivery', route: '', hidden: true },
],
},
{
label: 'menu.account',
icon: 'mdi-bank-outline',
disabled: false,
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'receipt', route: '/receipt' },
{ label: 'creditNote', route: '/credit-note' },
{ label: 'debitNote', route: '/debit-note' },
],
},
{
label: 'menu.manageDocument',
icon: 'mdi-archive-outline',
children: [
{
label: 'document',
route: '/document-management',
},
],
},
{
label: 'menu.overall',
icon: 'mdi-monitor-dashboard',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin', 'executive']),
children: [
{ label: 'report', route: '/report' },
{ label: 'dashboard', route: '/dash-board' },
],
},
{
label: 'menu.manual',
icon: 'mdi-book-open-variant-outline',
children: [
{
label: 'usage',
route: '/manual',
},
{
label: 'troubleshooting',
route: '/troubleshooting',
},
],
},
];
}
watch(
() => currentPath.value,
() => {
if (currentPath.value === '/') {
menuActive.value.fill(false);
menuActive.value[0] = true;
} else reActiveMenu();
},
);
watch(
() => props.mini,
() => {
if (props.mini) {
reActiveMenu();
}
},
);
watch(
() => locale.value,
() => {
initMenu();
},
);
onMounted(async () => {
const uid = getUserId();
role.value = getRole();
if (!uid) return;
if (role.value.includes('system')) {
const result = await userBranch.fetchListOptionBranch();
if (result && result.total > 0) currentMyBranch.value = result.result[0];
}
const result = await userBranch.fetchListMyBranch(uid);
if (result && result.total > 0) currentMyBranch.value = result.result[0];
initMenu();
menuActive.value = menuData.value.map(() => false);
if (currentPath.value === '/') menuActive.value[0] = true;
else reActiveMenu();
});
</script>
<template>
<!-- Drawer -->
<q-drawer
no-swipe-open
v-model="leftDrawerOpen"
side="left"
:breakpoint="599"
class="column justify-between no-wrap"
:class="{ dark: $q.dark.isActive }"
:width="mini ? 80 : 256"
show-if-above
>
<section
class="scroll"
style="overflow-x: hidden; scrollbar-gutter: stable"
>
<header
class="flex justify-center items-center q-pl-sm cursor-pointer"
@click="$router.push('/')"
id="btn-drawer-home"
style="height: 128px"
>
<q-img
fit="contain"
:src="mini ? '/logo_jws.png' : '/logo.png'"
:style="{ filter: `brightness(${$q.dark.isActive ? '1.3' : '1'})` }"
style="height: 64px"
/>
</header>
<div id="drawer-menu" class="q-pl-md q-mr-xs q-gutter-y-sm">
<template
v-for="(menu, i) in menuData.filter(
(v) =>
!(v.children?.length === 0 || v.children?.every((i) => i.hidden)),
)"
:key="i"
>
<q-expansion-item
v-if="!menu.hidden"
:id="menu.label"
:for="menu.label"
dense
:model-value="menuActive[i]"
expand-icon-class="no-padding"
:hide-expand-icon="mini"
:disable="menu.disabled"
:header-class="{
row: true,
'no-padding justify-center': mini,
'active-menu text-weight-bold': menuActive[i],
'text-weight-medium': !menu.disabled,
}"
@update:model-value="
() => {
mini ? '' : (menuActive[i] = !menuActive[i]);
}
"
>
<template #header>
<nav class="row items-center q-py-sm no-wrap">
<i
v-if="menu.isax"
:class="`isax ${menu.icon}`"
style="font-size: 24px"
/>
<Icon
v-else
:icon="menu.icon || ''"
width="24px"
:class="{ 'fix-icon': !menuActive[i] }"
/>
<span
v-if="!mini"
class="q-pl-sm"
style="white-space: nowrap"
:style="!menuActive[i] && `color: var(--foreground)`"
>
{{ menu.noI18n ? menu.label : $t(`${menu.label}.title`) }}
</span>
<q-tooltip :delay="500">
{{ menu.noI18n ? menu.label : $t(`${menu.label}.title`) }}
</q-tooltip>
<q-menu
v-if="mini && !menu.disabled"
fit
anchor="center end"
self="top left"
:offset="[16, 0]"
>
<q-list dense>
<template v-for="(sub, i) in menu.children" :key="i">
<q-item
v-if="!sub.hidden"
clickable
:disabled="sub.disabled"
class="row items-center no-wrap"
:class="{
'text-weight-bold app-bg-brand-1':
currentPath === sub.route,
}"
@click="
() => {
!sub.disabled && navigateTo('', sub.route);
}
"
:id="`sub-menu-${sub.label}`"
>
<span style="white-space: nowrap">
{{
sub.noI18n
? sub.label
: $t(`${menu.label}.${sub.label}`)
}}
</span>
</q-item>
</template>
</q-list>
</q-menu>
</nav>
</template>
<q-list v-if="!mini" dense>
<template v-for="(sub, i) in menu.children" :key="i">
<div
v-if="!sub.hidden"
class="row items-center q-ml-lg q-py-sm no-wrap"
:class="{ 'q-mt-sm': i === 0, 'sub-menu': !sub.disabled }"
style="border-left: 2px solid var(--surface-tab)"
@click="!sub.disabled && navigateTo('', sub.route)"
:id="`sub-menu-${sub.label}`"
>
<nav
class="row items-center no-wrap"
:class="{
active: sub.route && currentPath.includes(sub.route),
disabled: sub.disabled,
}"
>
<span class="q-px-md" style="white-space: nowrap">
{{
sub.noI18n
? sub.label
: $t(`${menu.label}.${sub.label}`)
}}
</span>
</nav>
</div>
</template>
</q-list>
</q-expansion-item>
</template>
<!-- <q-item
v-for="v in labelMenu.filter((v) => !v.hidden)"
:id="`drawer-${v.label}`"
dense
clickable
class="row items-center q-my-xs q-px-xs q-py-sm"
:key="`drawer-${v.label}`"
:disable="!!v.disabled"
:class="{
active: currentPath === v.route,
'border-active': currentPath === v.route,
dark: $q.dark.isActive,
}"
@click="navigateTo(v.label, v.route)"
>
<div class="row justify-center items-center">
<i
v-if="v.isax"
:class="`isax ${v.icon}`"
style="font-size: 24px"
/>
<Icon v-else :icon="v.icon" width="24px" />
<q-tooltip v-if="mini">
{{ $t(v.label) }}
</q-tooltip>
</div>
<div v-if="!mini" class="q-ml-md" style="white-space: nowrap">
{{ $t(v.label) }}
</div>
</q-item> -->
</div>
</section>
<footer
class="surface-2 q-px-md q-py-sm row items-center no-wrap justify-start"
:class="{ 'justify-center': mini }"
style="min-height: 56px; overflow-x: hidden"
>
<q-avatar
text-color="white"
size="md"
:style="`background-color: hsla(var(--violet-${$q.dark.isActive ? '10' : '11'}-hsl)/0.15)`"
class="relative-position"
>
<q-icon
name="mdi-home-group"
size="sm"
:style="`color: var(--violet-${$q.dark.isActive ? '10' : '11'})`"
/>
<div class="dot absolute-bottom-right" />
</q-avatar>
<div
v-if="!mini"
class="text-caption column q-mx-md col"
style="white-space: nowrap"
>
<span class="text-weight-bold ellipsis full-width">
{{
($i18n.locale === 'eng'
? currentMyBranch?.nameEN
: currentMyBranch?.name) ?? '-'
}}
<q-tooltip>
{{
($i18n.locale === 'eng'
? currentMyBranch?.nameEN
: currentMyBranch?.name) ?? '-'
}}
</q-tooltip>
</span>
<span>
{{ currentMyBranch?.code ?? '-' }}
</span>
</div>
<!-- v-if="!mini" -->
<q-btn
v-if="false"
dense
flat
rounded
icon="mdi-cog-outline"
size="sm"
@click="branchSetting"
style="margin-left: auto"
/>
</footer>
</q-drawer>
</template>
<style lang="scss" scoped>
#drawer-menu :deep(.q-item) {
color: var(--gray-7);
border-radius: var(--radius-2);
&.dark {
color: var(--gray-3);
}
}
#drawer-menu :deep(.q-item.active) {
--_drawer-item-background-color: var(--brand-1) !important;
background-color: var(--_drawer-item-background-color);
color: white;
// border-left: 10px solid $secondary;
}
#drawer-menu :deep(.q-item.active)::before {
display: block;
position: absolute;
content: ' ';
background: var(--brand-2);
border-radius: 99rem;
width: 6px;
left: -0.6rem;
top: 18%;
bottom: 18%;
cursor: context-menu;
}
:deep(.q-drawer) {
transition: 0.1s width ease-in-out;
background-color: transparent;
padding: var(--size-4);
padding-right: 0;
}
:deep(.q-drawer__content) {
border-radius: var(--radius-2);
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(11.2px);
-webkit-backdrop-filter: blur(11.2px);
&.dark {
background-color: hsla(var(--gray-10-hsl) / 0.5);
}
}
.dot {
height: 10px;
width: 10px;
background-color: var(--teal-6);
border-radius: 50%;
display: inline-block;
border: 1.5px solid white;
}
:deep(.active-menu) {
color: white !important;
background: var(--brand-1) !important;
&
i.q-icon.mdi.mdi-chevron-down.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated {
color: white !important;
}
}
.sub-menu {
font-weight: 400;
& .active {
color: var(--brand-1) !important;
font-weight: bold;
&::before {
content: '';
display: inline-block;
margin-left: -3px;
width: 4px;
height: 18px;
border-radius: 50px;
background-color: var(--brand-2);
}
}
&:hover {
cursor: pointer;
background-color: hsla(var(--stone-8-hsl) / 0.1);
transition: 0.3s ease-in-out;
border-top-right-radius: var(--radius-2);
border-bottom-right-radius: var(--radius-2);
}
}
.fix-icon {
color: var(--text-mute-2) !important;
}
:deep(.q-item.q-item-type.row.no-wrap.q-item--dense.disabled) {
opacity: 30% !important;
}
</style>