update sidebar dan menu layanan penunjang

This commit is contained in:
bagus-arie05
2025-10-14 08:49:54 +07:00
parent 51809468a9
commit 31e18c5965
16 changed files with 3257 additions and 1366 deletions

158
components/ProfileMenu.vue Normal file
View File

@@ -0,0 +1,158 @@
<template>
<v-menu
v-model="menu"
:close-on-content-click="false"
location="top end"
offset="8"
origin="bottom right"
transition="slide-y-transition"
>
<template v-slot:activator="{ props: menuProps }">
<div
v-bind="menuProps"
class="d-flex align-center cursor-pointer pa-2 rounded-lg hover-bg"
>
<v-avatar size="40">
<v-img
:src="user.picture"
:alt="`${user.name} Profile`"
></v-img>
<v-badge
dot
color="orange"
location="bottom right"
offset-x="2"
offset-y="2"
></v-badge>
</v-avatar>
<div v-show="!rail" class="ml-3 flex-grow-1">
<div class="text-subtitle-2 font-weight-bold">
{{ user.name }}
</div>
<div class="text-caption text-grey">
{{ user.email }}
</div>
</div>
<v-btn v-show="!rail" icon size="small" variant="text">
<v-icon size="20">mdi-dots-vertical</v-icon>
</v-btn>
</div>
</template>
<v-card width="300" elevation="8" rounded="lg">
<v-card-text class="pa-4">
<div class="d-flex align-center mb-4">
<v-avatar size="48">
<v-img :src="user.picture"></v-img>
</v-avatar>
<div class="ml-3">
<div class="text-subtitle-1 font-weight-bold">
{{ user.name }}
</div>
<div class="text-caption text-grey">
{{ user.email }}
</div>
</div>
</div>
<v-list density="compact" class="pa-0">
<v-list-item
prepend-icon="mdi-cog-outline"
class="px-2 rounded-lg"
link
@click="handleAction('setting')"
>
<v-list-item-title class="text-body-2">Setting</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-help-circle-outline"
class="px-2 rounded-lg"
link
@click="handleAction('help')"
>
<v-list-item-title class="text-body-2">Bantuan</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
prepend-icon="mdi-logout"
class="px-2 rounded-lg text-red"
link
@click="signOut"
:disabled="isLoggingOut"
>
<v-list-item-title class="text-body-2">
{{ isLoggingOut ? 'Log out...' : 'Log out' }}
</v-list-item-title>
</v-list-item>
</v-list>
<div class="text-caption text-grey mt-3 text-center">
v2.5.18 · Terms & Conditions
</div>
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup>
import { ref } from 'vue';
// --- PROPS & EMITS ---
const props = defineProps({
user: {
type: Object,
// Diubah menjadi required: false dengan default yang aman
required: false,
default: () => ({
name: 'Guest',
email: 'guest@rssa.com',
picture: 'https://i.pravatar.cc/150?img=33'
})
},
rail: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['logout']); // Hanya emit logout
// --- STATE ---
const menu = ref(false);
const isLoggingOut = ref(false);
// --- METHODS ---
const signOut = () => {
if (isLoggingOut.value) return;
isLoggingOut.value = true;
menu.value = false;
// Memicu event untuk ditangani oleh komponen induk (SideBar)
emit('logout');
// Biarkan parent yang mengurus redirect/state global
setTimeout(() => isLoggingOut.value = false, 1000);
};
const handleAction = (action) => {
console.log(`[PopupSidebar] Action: ${action} triggered.`);
// TODO: Tambahkan logika navigasi/fungsi di sini, misal: router.push('/settings')
menu.value = false;
};
</script>
<style scoped>
/* Tambahkan kembali style yang relevan untuk aktivator di sini */
.hover-bg:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
.text-red {
color: rgb(244, 67, 54) !important;
}
</style>

View File

@@ -1,103 +1,260 @@
<!-- components/sideBar.vue -->
<template>
<v-navigation-drawer
:model-value="drawer"
v-model="drawer"
:rail="rail"
permanent
app
@update:model-value="emit('update:drawer', $event)"
color="white"
width="280"
rail-width="72"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<v-list density="compact" nav>
<template v-for="item in items" :key="item.name">
<div class="pa-4 d-flex align-center" style="height: 64px">
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
Antrean RSSA
</v-toolbar-title>
</div>
<v-divider></v-divider>
<v-list density="default" nav class="py-2">
<!-- search bar
<v-list-item
prepend-icon="mdi-magnify"
:title="!rail ? 'Search' : ''"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item> -->
<template v-for="item in navItemsStore.getNavItems" :key="item.id">
<v-menu
v-if="item.children"
open-on-hover
:location="rail ? 'end' : undefined"
:offset="10"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.name"
:value="item.name"
:to="!rail ? item.path : undefined"
:link="!rail"
></v-list-item>
</template>
<v-card class="py-2" min-width="200">
<v-card-title class="text-subtitle-1 font-weight-bold px-4 py-2">
{{ item.name }}
</v-card-title>
<v-divider></v-divider>
<v-list density="compact" nav>
<v-list-item
v-for="child in item.children"
:key="child.name"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="px-4"
></v-list-item>
</v-list>
</v-card>
</v-menu>
<v-tooltip
v-else
:disabled="!rail"
v-if="item.children && item.children.length > 0"
open-on-hover
location="end"
:text="item.name"
:disabled="!rail"
>
<template #activator="{ props }">
<template v-slot:activator="{ props }">
<v-list-group v-if="!rail" :value="item.name">
<template v-slot:activator="{ props: listGroupProps }">
<v-list-item
v-bind="listGroupProps"
:prepend-icon="item.icon"
:title="item.name"
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1 pl-12"
>
</v-list-item>
</v-list-group>
<v-list-item
v-else
v-bind="props"
:prepend-icon="item.icon"
:title="item.name"
:to="item.path"
link
></v-list-item>
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
</v-tooltip>
<v-list class="py-2" style="min-width: 200px">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ item.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2"
>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-else
:prepend-icon="item.icon"
:title="!rail ? item.name : ''"
:to="item.path"
:active="item.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1"
>
<template v-slot:append v-if="item.badge && !rail">
<v-chip size="x-small" color="error" variant="flat">{{
item.badge
}}</v-chip>
</template>
</v-list-item>
</template>
<v-divider class="my-4 mx-2"></v-divider>
</v-list>
<v-spacer></v-spacer>
<template v-slot:append>
<div class="pa-3">
<ProfileMenu
:user="user.value" :rail="rail"
@logout="handleLogout"
class="full-width-on-expand"
/>
</div>
</template>
</v-navigation-drawer>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
<script setup>
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
// Import store Pinia
import { useNavItemsStore } from "@/stores/navItems"; // Sesuaikan path ini jika perlu
import ProfileMenu from "./ProfileMenu.vue";
interface NavItem {
id: number;
name: string;
path: string;
icon: string;
children?: NavItem[];
}
// State
const drawer = ref(true);
const rail = ref(true);
const hoverTimeout = ref(null);
const props = defineProps({
items: {
type: Array as () => NavItem[],
required: true,
},
rail: {
type: Boolean,
required: true,
},
drawer: {
type: Boolean,
required: true,
},
// Akses Pinia Store
const user = ref({
name: 'Adam Sulfat',
email: 'adam@rssa.com',
picture: 'https://i.pravatar.cc/150?img=33', // Ganti dengan URL gambar dinamis
id: 'a1b2c3d4e5f6g7h8'
});
const navItemsStore = useNavItemsStore();
const currentRoute = useRoute();
const currentActiveMenu = computed(() => {
// Menggunakan Getter untuk mendapatkan array menu yang pasti valid
const items = navItemsStore.getNavItems;
// Pengecekan keamanan (walaupun Getter seharusnya sudah menjamin array)
if (!Array.isArray(items) || items.length === 0) {
return "";
}
// Cari item utama yang sedang aktif
const currentItem = items.find((item) => item.path === currentRoute.path);
if (currentItem) {
return currentItem.name;
}
// Cari item anak (children) yang sedang aktif
for (const item of items) {
if (item.children && Array.isArray(item.children)) {
const childItem = item.children.find(
(child) => child.path === currentRoute.path
);
if (childItem) {
return item.name;
}
}
}
return "";
});
const emit = defineEmits(['update:drawer']);
// Hover handlers
const handleMouseEnter = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
}
hoverTimeout.value = setTimeout(() => {
rail.value = false;
}, 100);
};
const handleMouseLeave = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
}
hoverTimeout.value = setTimeout(() => {
rail.value = true;
}, 200);
};
const handleLogout = () => {
console.log("Logging out...");
// Tambahkan logika logout di sini
};
</script>
<style scoped>
.v-navigation-drawer__content {
background-color: #ffffff;
.v-list-item--active {
background-color: #e3f2fd !important;
color: #1976d2 !important;
}
.v-list-item--active :deep(.v-list-item__prepend) {
color: #1976d2 !important;
}
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
.v-navigation-drawer {
border-right: 1px solid #e0e0e0 !important;
transition: width 0.2s ease-in-out !important;
}
.hover-bg:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
/* Custom scrollbar */
:deep(.v-navigation-drawer__content) {
overflow-y: auto;
overflow-x: hidden;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar {
width: 6px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-track {
background: transparent;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>