Merge pull request #158 from dikstub-rssa/feat/menu-structure
Feat/menu structure
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const navMenu = ref([])
|
type NavGroup = {
|
||||||
|
heading?: string
|
||||||
|
items: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getActiveRole } = useUserStore()
|
||||||
|
|
||||||
|
const navMenu = ref<NavGroup[]>([])
|
||||||
|
|
||||||
const teams: {
|
const teams: {
|
||||||
name: string
|
name: string
|
||||||
@@ -12,7 +19,14 @@ const teams: {
|
|||||||
plan: 'Saiful Anwar Hospital',
|
plan: 'Saiful Anwar Hospital',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const sidebar = {
|
|
||||||
|
type Sidebar = {
|
||||||
|
collapsible: 'offcanvas' | 'icon' | 'none'
|
||||||
|
side: 'left' | 'right'
|
||||||
|
variant: 'sidebar' | 'floating' | 'inset'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebar: Sidebar = {
|
||||||
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
|
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
|
||||||
side: 'left', // 'left' | 'right'
|
side: 'left', // 'left' | 'right'
|
||||||
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
|
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
|
||||||
@@ -29,18 +43,34 @@ onMounted(async () => {
|
|||||||
await setMenu()
|
await setMenu()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function setMenu() {
|
||||||
|
const activeRole = getActiveRole()
|
||||||
|
const activeRoleParts = activeRole ? activeRole.split('|') : []
|
||||||
|
const role = activeRoleParts[0]+(activeRoleParts.length > 1 ? `-${activeRoleParts[1]}` : '')
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/side-menu-items/${role.toLowerCase()}.json`)
|
||||||
|
const rawMenu = await res.text()
|
||||||
|
navMenu.value = JSON.parse(rawMenu)
|
||||||
|
} catch (e) {
|
||||||
|
const res = await fetch(`/side-menu-items/blank.json`)
|
||||||
|
const rawMenu = await res.text()
|
||||||
|
navMenu.value = JSON.parse(rawMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveNavItemComponent(item: any): any {
|
function resolveNavItemComponent(item: any): any {
|
||||||
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
|
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
|
||||||
|
|
||||||
return resolveComponent('LayoutSidebarNavLink')
|
return resolveComponent('LayoutSidebarNavLink')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setMenu() {
|
watch(getActiveRole, async () => {
|
||||||
const position_code = 'sys'
|
await setMenu()
|
||||||
const res = await fetch(`/side-menu-items/${position_code}.json`)
|
// const activeRole = getActiveRole()
|
||||||
const rawMenu = await res.text()
|
// const res = await fetch(`/side-menu-items/${activeRole}.json`)
|
||||||
navMenu.value = JSON.parse(rawMenu)
|
// const rawMenu = await res.text()
|
||||||
}
|
// navMenu.value = JSON.parse(rawMenu)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { useSidebar } from '~/components/pub/ui/sidebar'
|
|||||||
// }>()
|
// }>()
|
||||||
|
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar()
|
||||||
const { logout } = useUserStore()
|
const { user, logout, setActiveRole, getActiveRole } = useUserStore()
|
||||||
const userStore = useUserStore().user
|
// const userStore = useUserStore().user
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
navigateTo('/auth/login')
|
navigateTo('/auth/login')
|
||||||
@@ -32,19 +32,19 @@ const showModalTheme = ref(false)
|
|||||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar class="h-8 w-8 rounded-lg">
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src="" :alt="userStore?.user_name || 'system'" />
|
<AvatarImage src="" :alt="user.user_name || 'system'" />
|
||||||
<AvatarFallback class="rounded-lg">
|
<AvatarFallback class="rounded-lg">
|
||||||
{{
|
{{
|
||||||
userStore?.user_name
|
user.user_name
|
||||||
?.split(' ')
|
?.split(' ')
|
||||||
.map((n) => n[0])
|
.map((n: string) => n[0])
|
||||||
.join('') || ''
|
.join('') || ''
|
||||||
}}
|
}}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
|
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
|
||||||
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
|
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
|
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@@ -52,35 +52,40 @@ const showModalTheme = ref(false)
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg bg-white"
|
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg bg-white"
|
||||||
:side="isMobile ? 'bottom' : 'right'"
|
:side="isMobile ? 'bottom' : 'right'"
|
||||||
align="end"
|
:align="'end'"
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel class="p-0 font-normal">
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<Avatar class="h-8 w-8 rounded-lg">
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage src="" :alt="userStore?.user_name || 'system'" />
|
<AvatarImage src="" :alt="user.user_name || 'system'" />
|
||||||
<AvatarFallback class="rounded-lg">
|
<AvatarFallback class="rounded-lg">
|
||||||
{{
|
{{
|
||||||
userStore?.user_name
|
user.user_name
|
||||||
?.split(' ')
|
?.split(' ')
|
||||||
.map((n) => n[0])
|
.map((n: string) => n[0])
|
||||||
.join('') || ''
|
.join('') || ''
|
||||||
}}
|
}}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
|
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
|
||||||
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
|
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
|
||||||
<DropdownMenuGroup>
|
<Icon name="i-lucide-user" />
|
||||||
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
|
Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<template v-if="user.roles.length > 1">
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem v-for="role in user.roles" class="hover:bg-gray-100" @click="setActiveRole(role)">
|
||||||
<Icon name="i-lucide-user" />
|
<Icon name="i-lucide-user" />
|
||||||
Profile
|
{{ role }}
|
||||||
|
<Icon name="i-lucide-check" class="ml-auto" v-if="getActiveRole() === role" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</template>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem class="hover:bg-gray-100" @click="handleLogout">
|
<DropdownMenuItem class="hover:bg-gray-100" @click="handleLogout">
|
||||||
<Icon name="i-lucide-log-out" />
|
<Icon name="i-lucide-log-out" />
|
||||||
|
|||||||
+48
-48
@@ -2,67 +2,67 @@ import type { RoleAccess } from '~/models/role'
|
|||||||
|
|
||||||
export const PAGE_PERMISSIONS = {
|
export const PAGE_PERMISSIONS = {
|
||||||
'/patient': {
|
'/patient': {
|
||||||
doctor: ['R'],
|
'emp|doc': ['R'],
|
||||||
nurse: ['R'],
|
'emp|nur': ['R'],
|
||||||
admisi: ['C', 'R', 'U', 'D'],
|
'emp|reg': ['C', 'R', 'U', 'D'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/doctor': {
|
'/doctor': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['R'],
|
'emp|nur': ['R'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/satusehat': {
|
'/satusehat': {
|
||||||
doctor: ['R'],
|
'emp|doc': ['R'],
|
||||||
nurse: ['R'],
|
'emp|nur': ['R'],
|
||||||
admisi: ['C', 'R', 'U', 'D'],
|
'emp|reg': ['C', 'R', 'U', 'D'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/outpatient/encounter': {
|
'/outpatient/encounter': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['C', 'R', 'U', 'D'],
|
'emp|nur': ['C', 'R', 'U', 'D'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/emergency/encounter': {
|
'/emergency/encounter': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['C', 'R', 'U', 'D'],
|
'emp|nur': ['C', 'R', 'U', 'D'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/inpatient/encounter': {
|
'/inpatient/encounter': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['C', 'R', 'U', 'D'],
|
'emp|nur': ['C', 'R', 'U', 'D'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/rehab/encounter': {
|
'/rehab/encounter': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['R'],
|
'emp|nur': ['R'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/rehab/registration': {
|
'/rehab/registration': {
|
||||||
doctor: ['C', 'R', 'U', 'D'],
|
'emp|doc': ['C', 'R', 'U', 'D'],
|
||||||
nurse: ['R'],
|
'emp|nur': ['R'],
|
||||||
admisi: ['R'],
|
'emp|reg': ['R'],
|
||||||
pharmacy: ['R'],
|
'emp|pha': ['R'],
|
||||||
billing: ['R'],
|
'emp|pay': ['R'],
|
||||||
management: ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
} as const satisfies Record<string, RoleAccess>
|
} as const satisfies Record<string, RoleAccess>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
|||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['system', 'emp-doc', 'emp-nur', 'emp-reg', 'emp-pha', 'emp-pay', 'emp-mng'],
|
||||||
title: 'Daftar Kunjungan',
|
title: 'Daftar Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
@@ -23,7 +23,7 @@ const { checkRole, hasReadAccess } = useRBAC()
|
|||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>Examination</div>
|
|
||||||
</template>
|
|
||||||
+24
-5
@@ -5,12 +5,13 @@ export const useUserStore = defineStore(
|
|||||||
// const token = useCookie('authentication')
|
// const token = useCookie('authentication')
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!user.value)
|
const isAuthenticated = computed(() => !!user.value)
|
||||||
|
|
||||||
const userRole = computed(() => {
|
const userRole = computed(() => {
|
||||||
const roles = user.value?.roles || []
|
return user.value?.roles || []
|
||||||
return roles.map((input: string) => {
|
// return roles.map((input: string) => {
|
||||||
const parts = input.split('-')
|
// const parts = input.split('|')
|
||||||
return parts.length > 1 ? parts[1]: parts[0]
|
// return parts.length > 1 ? parts[1]: parts[0]
|
||||||
})
|
// })
|
||||||
})
|
})
|
||||||
|
|
||||||
const login = async (userData: any) => {
|
const login = async (userData: any) => {
|
||||||
@@ -21,12 +22,30 @@ export const useUserStore = defineStore(
|
|||||||
user.value = null
|
user.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setActiveRole = (role: string) => {
|
||||||
|
if (user.value && user.value.roles.includes(role)) {
|
||||||
|
user.value.activeRole = role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActiveRole = () => {
|
||||||
|
if (user.value?.activeRole) {
|
||||||
|
return user.value.activeRole
|
||||||
|
}
|
||||||
|
if (user.value?.roles.length > 0) {
|
||||||
|
user.value.activeRole = user.value.roles[0]
|
||||||
|
return user.value.activeRole
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
userRole,
|
userRole,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
setActiveRole,
|
||||||
|
getActiveRole,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"heading": "Menu Utama",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Dashboard",
|
||||||
|
"icon": "i-lucide-home",
|
||||||
|
"link": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"heading": "Menu Utama",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Dashboard",
|
||||||
|
"icon": "i-lucide-home",
|
||||||
|
"link": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Rehabilitasi Medik",
|
"title": "Rehabilitasi Medik",
|
||||||
"icon": "i-lucide-bike",
|
"icon": "i-lucide-bike",
|
||||||
"link": "/rehab/polyclinic-queue"
|
"link": "/rehab/encounter-queue"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Rawat Inap",
|
"title": "Rawat Inap",
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "Antrian Poliklinik",
|
"title": "Antrian Poliklinik",
|
||||||
"link": "/outpatient/polyclinic-queue"
|
"link": "/outpatient/encounter-queue"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Pendaftaran",
|
"title": "Kunjungan",
|
||||||
"link": "/outpatient/encounter"
|
"link": "/outpatient/encounter"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"link": "/emergency/triage"
|
"link": "/emergency/triage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Pemeriksaan",
|
"title": "Kunjungan",
|
||||||
"link": "/emergency/encounter"
|
"link": "/emergency/encounter"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"title": "Antrean Poliklinik",
|
"title": "Antrean Poliklinik",
|
||||||
"link": "/rehab/polyclinic-queue"
|
"link": "/rehab/encounter-queue"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Kunjungan",
|
"title": "Kunjungan",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Antrian Poliklinik",
|
"title": "Antrian Poliklinik",
|
||||||
"link": "/outpatient/polyclinic-queue"
|
"link": "/outpatient/encounter-queue"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Kunjungan",
|
"title": "Kunjungan",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Antrean Poliklinik",
|
"title": "Antrean Poliklinik",
|
||||||
"link": "/rehab/polyclinic-queue"
|
"link": "/rehab/encounter-queue"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Kunjungan",
|
"title": "Kunjungan",
|
||||||
Reference in New Issue
Block a user