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">
const navMenu = ref([])
type NavGroup = {
heading?: string
items: any[]
}
const { getActiveRole } = useUserStore()
const navMenu = ref<NavGroup[]>([])
const teams: {
name: string
@@ -12,7 +19,14 @@ const teams: {
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'
side: 'left', // 'left' | 'right'
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
@@ -29,18 +43,34 @@ onMounted(async () => {
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 {
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
return resolveComponent('LayoutSidebarNavLink')
}
async function setMenu() {
const position_code = 'sys'
const res = await fetch(`/side-menu-items/${position_code}.json`)
const rawMenu = await res.text()
navMenu.value = JSON.parse(rawMenu)
}
watch(getActiveRole, async () => {
await setMenu()
// const activeRole = getActiveRole()
// const res = await fetch(`/side-menu-items/${activeRole}.json`)
// const rawMenu = await res.text()
// navMenu.value = JSON.parse(rawMenu)
})
</script>
<template>
+23 -18
View File
@@ -10,8 +10,8 @@ import { useSidebar } from '~/components/pub/ui/sidebar'
// }>()
const { isMobile } = useSidebar()
const { logout } = useUserStore()
const userStore = useUserStore().user
const { user, logout, setActiveRole, getActiveRole } = useUserStore()
// const userStore = useUserStore().user
function handleLogout() {
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"
>
<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">
{{
userStore?.user_name
user.user_name
?.split(' ')
.map((n) => n[0])
.map((n: string) => n[0])
.join('') || ''
}}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
</div>
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
</SidebarMenuButton>
@@ -52,35 +52,40 @@ const showModalTheme = ref(false)
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg bg-white"
:side="isMobile ? 'bottom' : 'right'"
align="end"
:align="'end'"
>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<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">
{{
userStore?.user_name
user.user_name
?.split(' ')
.map((n) => n[0])
.map((n: string) => n[0])
.join('') || ''
}}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
<Icon name="i-lucide-user" />
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" />
Profile
{{ role }}
<Icon name="i-lucide-check" class="ml-auto" v-if="getActiveRole() === role" />
</DropdownMenuItem>
</DropdownMenuGroup>
</template>
<DropdownMenuSeparator />
<DropdownMenuItem class="hover:bg-gray-100" @click="handleLogout">
<Icon name="i-lucide-log-out" />
+48 -48
View File
@@ -2,67 +2,67 @@ import type { RoleAccess } from '~/models/role'
export const PAGE_PERMISSIONS = {
'/patient': {
doctor: ['R'],
nurse: ['R'],
admisi: ['C', 'R', 'U', 'D'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/doctor': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['R'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['R'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/satusehat': {
doctor: ['R'],
nurse: ['R'],
admisi: ['C', 'R', 'U', 'D'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/outpatient/encounter': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['C', 'R', 'U', 'D'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/emergency/encounter': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['C', 'R', 'U', 'D'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/inpatient/encounter': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['C', 'R', 'U', 'D'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/rehab/encounter': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['R'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['R'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/rehab/registration': {
doctor: ['C', 'R', 'U', 'D'],
nurse: ['R'],
admisi: ['R'],
pharmacy: ['R'],
billing: ['R'],
management: ['R'],
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['R'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
},
} as const satisfies Record<string, RoleAccess>
@@ -5,7 +5,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
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',
contentFrame: 'cf-full-width',
})
@@ -23,7 +23,7 @@ const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
// navigateTo('/403')
}
// 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 isAuthenticated = computed(() => !!user.value)
const userRole = computed(() => {
const roles = user.value?.roles || []
return roles.map((input: string) => {
const parts = input.split('-')
return parts.length > 1 ? parts[1]: parts[0]
})
return user.value?.roles || []
// return roles.map((input: string) => {
// const parts = input.split('|')
// return parts.length > 1 ? parts[1]: parts[0]
// })
})
const login = async (userData: any) => {
@@ -21,12 +22,30 @@ export const useUserStore = defineStore(
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 {
user,
isAuthenticated,
userRole,
login,
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",
"icon": "i-lucide-bike",
"link": "/rehab/polyclinic-queue"
"link": "/rehab/encounter-queue"
},
{
"title": "Rawat Inap",
@@ -13,10 +13,10 @@
"children": [
{
"title": "Antrian Poliklinik",
"link": "/outpatient/polyclinic-queue"
"link": "/outpatient/encounter-queue"
},
{
"title": "Pendaftaran",
"title": "Kunjungan",
"link": "/outpatient/encounter"
}
]
@@ -30,7 +30,7 @@
"link": "/emergency/triage"
},
{
"title": "Pemeriksaan",
"title": "Kunjungan",
"link": "/emergency/encounter"
}
]
@@ -42,7 +42,7 @@
"children": [
{
"title": "Antrean Poliklinik",
"link": "/rehab/polyclinic-queue"
"link": "/rehab/encounter-queue"
},
{
"title": "Kunjungan",
@@ -17,7 +17,7 @@
},
{
"title": "Antrian Poliklinik",
"link": "/outpatient/polyclinic-queue"
"link": "/outpatient/encounter-queue"
},
{
"title": "Kunjungan",
@@ -57,7 +57,7 @@
},
{
"title": "Antrean Poliklinik",
"link": "/rehab/polyclinic-queue"
"link": "/rehab/encounter-queue"
},
{
"title": "Kunjungan",