Merge pull request #158 from dikstub-rssa/feat/menu-structure

Feat/menu structure
This commit is contained in:
Munawwirul Jamal
2025-11-12 07:12:40 +07:00
committed by GitHub
17 changed files with 166 additions and 91 deletions
+38 -8
View File
@@ -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>
+23 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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,
} }
}, },
{ {
+12
View File
@@ -0,0 +1,12 @@
[
{
"heading": "Menu Utama",
"items": [
{
"title": "Dashboard",
"icon": "i-lucide-home",
"link": "/"
}
]
}
]
+12
View File
@@ -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",