akbar first commit

This commit is contained in:
Fanrouver
2025-10-01 11:28:06 +07:00
parent 55d8920a82
commit d735824d57
35 changed files with 6091 additions and 890 deletions

106
components/AppBar.vue Normal file
View File

@@ -0,0 +1,106 @@
<template>
<v-app-bar app color="#ff9248" dark>
<v-app-bar-nav-icon @click="emit('toggle-rail')"></v-app-bar-nav-icon>
<v-toolbar-title class="ml-2 font-weight-bold">
<span class="text-blue-darken-2">Antrean</span> RSSA
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Show loading state or user info -->
<div v-if="isLoading" class="d-flex align-center">
<v-progress-circular indeterminate size="20" class="mr-2"></v-progress-circular>
<span class="mr-2">Loading...</span>
</div>
<template v-else-if="isAuthenticated && user">
<ProfilePopup
:user="user"
@logout="handleLogout"
/><template>
<v-app-bar app color="blue-grey-darken-3" dark flat>
<v-app-bar-nav-icon @click="emit('toggle-rail')"></v-app-bar-nav-icon>
<v-toolbar-title class="ml-2 font-weight-bold">
<span class="text-orange-darken-2">Antrian</span> RSSA
</v-toolbar-title>
<v-spacer></v-spacer>
<div v-if="isLoading" class="d-flex align-center">
<v-progress-circular indeterminate color="orange-darken-2" size="20" class="mr-2"></v-progress-circular>
<span class="text-caption">Loading...</span>
</div>
<template v-else-if="isAuthenticated && user">
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
variant="flat"
rounded="xl"
color="transparent"
class="pa-2 text-capitalize"
>
<div class="d-flex align-center">
<v-avatar color="orange-darken-2" size="36" class="mr-2">
<span class="text-white font-weight-bold">{{ user.name?.charAt(0) || 'U' }}</span>
</v-avatar>
<span class="text-subtitle-1 font-weight-bold text-white">{{ user.name || 'User' }}</span>
<v-icon right size="20" class="ml-1">mdi-chevron-down</v-icon>
</div>
</v-btn>
</template>
<ProfilePopup
:user="user"
@logout="handleLogout"
/>
</v-menu>
</template>
<template v-else>
<v-btn @click="redirectToLogin" color="orange-darken-2" variant="flat" rounded="lg" class="text-capitalize">
<v-icon left>mdi-login</v-icon>
Login
</v-btn>
</template>
</v-app-bar>
</template>
<span class="mr-2">{{ user.name || user.preferred_username || user.email }}</span>
</template>
<template v-else>
<v-btn @click="redirectToLogin" color="white" text>
Login
</v-btn>
</template>
</v-app-bar>
</template>
<script setup>
import ProfilePopup from './ProfilePopup.vue';
// Emit untuk parent component
const emit = defineEmits(['toggle-rail']);
// Use auth composable
const { user, isAuthenticated, isLoading, checkAuth, logout } = useAuth()
// Handle logout - use the composable's logout method
const handleLogout = async () => {
console.log("🚪 AppBar logout initiated...")
try {
await logout()
} catch (error) {
console.error("❌ AppBar logout error:", error)
}
}
// Redirect to login if not authenticated
const redirectToLogin = () => {
navigateTo('/LoginPage')
}
// Check authentication on mount
onMounted(async () => {
await checkAuth()
})
</script>

View File

@@ -0,0 +1,124 @@
<template>
<v-card class="pa-4 rounded-lg elevation-2">
<v-card-title class="text-h5 font-weight-bold mb-4">
Edit Hak Akses Menu | {{ localItem.namaTipeUser }}
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12">
<v-card-title class="text-subtitle-1 font-weight-bold pa-0 mb-4">Hak Akses Menu</v-card-title>
<v-table density="compact" class="elevation-1 rounded-lg">
<thead>
<tr>
<th class="text-left">No</th>
<th class="text-left">Menu</th>
<th class="text-center">Akses</th>
<th class="text-center">Lihat</th>
<th class="text-center">Tambah</th>
<th class="text-center">Edit</th>
<th class="text-center">Hapus</th>
</tr>
</thead>
<tbody>
<tr v-for="(menu, index) in localItem.hakAksesMenu" :key="menu.name">
<td>{{ index + 1 }}</td>
<td>{{ menu.name }}</td>
<td class="text-center">
<v-checkbox v-model="menu.canAccess" hide-details></v-checkbox>
</td>
<td class="text-center">
<v-checkbox v-model="menu.canView" hide-details></v-checkbox>
</td>
<td class="text-center">
<v-checkbox v-model="menu.canAdd" hide-details></v-checkbox>
</td>
<td class="text-center">
<v-checkbox v-model="menu.canEdit" hide-details></v-checkbox>
</td>
<td class="text-center">
<v-checkbox v-model="menu.canDelete" hide-details></v-checkbox>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="d-flex justify-end pa-4">
<v-btn
color="grey-darken-1"
variant="flat"
class="text-capitalize rounded-lg mr-2"
@click="$emit('cancel')"
>
Batal
</v-btn>
<v-btn
color="orange-darken-2"
variant="flat"
class="text-capitalize rounded-lg"
@click="$emit('save', localItem)"
>
Submit
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
// Define types for better readability and safety
interface HakAksesMenu {
name: string;
canAccess: boolean;
canView: boolean;
canAdd: boolean;
canEdit: boolean;
canDelete: boolean;
}
interface HakAksesData {
id: number;
namaTipeUser: string;
hakAksesMenu: HakAksesMenu[];
}
// Define props with type validation
const props = defineProps({
item: {
type: Object as () => HakAksesData,
required: true,
// Add custom validator for more robust checks
validator: (value: HakAksesData) => {
return 'namaTipeUser' in value && 'hakAksesMenu' in value;
},
},
});
// Define emits for clarity
const emits = defineEmits(['save', 'cancel']);
// Use a local copy to avoid mutating the prop directly
const localItem = ref<HakAksesData>(JSON.parse(JSON.stringify(props.item)));
// Watch the prop for changes and update the local copy
watch(() => props.item, (newItem) => {
localItem.value = JSON.parse(JSON.stringify(newItem));
});
</script>
<style scoped>
.v-table :deep(th) {
font-weight: bold !important;
background-color: #f9fafb !important;
}
.v-table :deep(td) {
vertical-align: middle;
}
.v-checkbox :deep(.v-selection-control__input) {
color: #2196F3 !important;
}
</style>

View File

@@ -2,43 +2,89 @@
<v-menu
v-model="menu"
:close-on-content-click="false"
location="bottom"
location="bottom right"
origin="top right"
transition="scale-transition"
transition="slide-y-transition"
>
<template v-slot:activator="{ props }">
<v-btn
icon
v-bind="props"
class="ml-auto"
>
<v-icon size="40" color="#000000">mdi-account</v-icon>
<v-btn icon v-bind="props">
<v-avatar size="40">
<v-img
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
:alt="`${user?.name || 'User'} Profile`"
></v-img>
</v-avatar>
</v-btn>
</template>
<v-card class="mx-auto" color="#FFA000" dark width="250">
<v-list-item three-line class="py-4">
<v-list-item-title class="text-h6 text-center font-weight-bold">
<v-avatar color="#fff" size="60">
<v-icon size="40" color="#4CAF50">mdi-account</v-icon>
</v-avatar>
</v-list-item-title>
<v-list-item-subtitle class="text-center mt-2 text-black">
<span class="d-block font-weight-bold">Rajal Bayu Nogroho</span>
<span class="d-block">Super Admin</span>
</v-list-item-subtitle>
</v-list-item>
<v-card class="rounded-lg elevation-4 pa-4" width="300">
<div class="d-flex align-center pb-2">
<v-avatar size="48">
<v-img
:src="user?.picture || 'https://i.pravatar.cc/300?img=68'"
:alt="`${user?.name || 'User'} Profile`"
></v-img>
</v-avatar>
<div class="ml-4">
<div class="text-subtitle-1 font-weight-bold">
{{ user?.name || user?.preferred_username || 'User' }}
</div>
<div class="text-caption text-grey-darken-1">
{{ user?.email || 'No email' }}
</div>
<div class="text-caption text-grey-darken-2">
ID: {{ user?.id?.substring(0, 8) }}...
</div>
</div>
</div>
<v-divider class="my-2"></v-divider>
<v-card-actions class="d-flex justify-center pa-2">
<v-btn
color="black"
variant="text"
class="font-weight-bold"
<v-list dense>
<v-list-item link class="rounded-lg" @click="handleAction('account')">
<template v-slot:prepend>
<v-icon>mdi-cog</v-icon>
</template>
<v-list-item-title>Pengaturan Akun</v-list-item-title>
</v-list-item>
<v-list-item link class="rounded-lg" @click="handleAction('darkMode')">
<template v-slot:prepend>
<v-icon>mdi-weather-night</v-icon>
</template>
<v-list-item-title>Mode Gelap</v-list-item-title>
<template v-slot:append>
<v-switch
v-model="darkMode"
hide-details
density="compact"
color="primary"
></v-switch>
</template>
</v-list-item>
<v-list-item link class="rounded-lg" @click="handleAction('profile')">
<template v-slot:prepend>
<v-icon>mdi-account</v-icon>
</template>
<v-list-item-title>Profil Saya</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
link
class="rounded-lg text-red"
@click="signOut"
:disabled="isLoggingOut"
>
Sign out
</v-btn>
</v-card-actions>
<template v-slot:prepend>
<v-icon color="red">mdi-logout</v-icon>
</template>
<v-list-item-title>
{{ isLoggingOut ? 'Logging out...' : 'Keluar' }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</template>
@@ -46,15 +92,64 @@
<script setup>
import { ref } from 'vue';
const menu = ref(false);
// Props
const props = defineProps({
user: {
type: Object,
required: true
}
})
const signOut = () => {
console.log("Sign out button clicked!");
const menu = ref(false);
const darkMode = ref(false);
const isLoggingOut = ref(false);
const emit = defineEmits(['logout']);
/**
* Handles the logout action - delegates to parent
*/
const signOut = async () => {
if (isLoggingOut.value) return;
isLoggingOut.value = true;
menu.value = false;
// Implementasi logika logout di sini
try {
console.log('🚪 ProfilePopup signOut called...')
emit('logout');
} finally {
isLoggingOut.value = false;
}
};
const handleAction = (action) => {
console.log('Action triggered:', action);
switch(action) {
case 'account':
// Navigate to account settings
navigateTo('/settings/account')
break;
case 'profile':
// Navigate to profile page
navigateTo('/profile')
break;
case 'darkMode':
// Dark mode toggle is handled by v-model
break;
default:
console.log('Unknown action:', action);
}
// Close menu for navigation actions
if (action !== 'darkMode') {
menu.value = false;
}
};
</script>
<style scoped>
/* No specific scoped styles needed for this component as Vuetify classes handle styling */
</style>
.text-red {
color: rgb(244, 67, 54) !important;
}
</style>

View File

@@ -0,0 +1,75 @@
<!-- <template>
<v-dialog v-model="dialog" max-width="500px">
<v-card>
<v-card-title class="text-h6 font-weight-bold">
Atur Urutan Menu
</v-card-title>
<v-card-text>
<v-list dense>
<draggable v-model="localMenus" item-key="title" @end="onDragEnd">
<template #item="{ element }">
<v-list-item class="reorder-item">
<v-list-item-content>
<v-list-item-title>{{ element.title }}</v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-icon>mdi-drag</v-icon>
</v-list-item-icon>
</v-list-item>
</template>
</draggable>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" text @click="dialog = false">Batal</v-btn>
<v-btn color="blue" text @click="saveOrder">Simpan Urutan</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import draggable from 'vuedraggable';
import navigationItems from '~/data/menu.js';
const dialog = ref(false);
const localMenus = ref([]);
// Watch for changes in the dialog's visibility
watch(dialog, (val) => {
if (val) {
// Make a deep copy to avoid mutating the original data
localMenus.value = JSON.parse(JSON.stringify(navigationItems));
}
});
const onDragEnd = (event) => {
// Logic to handle the end of a drag event
};
const saveOrder = () => {
// Emit an event with the new menu order
// You would then handle this in the parent component
// to update the ~/data/menu.js file (or your state management)
// and trigger a UI refresh.
dialog.value = false;
};
const openDialog = () => {
dialog.value = true;
};
defineExpose({ openDialog });
</script>
<style scoped>
.reorder-item {
cursor: grab;
border-bottom: 1px solid #eee;
}
.reorder-item:active {
cursor: grabbing;
}
</style> -->

103
components/SideBar.vue Normal file
View File

@@ -0,0 +1,103 @@
<!-- components/sideBar.vue -->
<template>
<v-navigation-drawer
:model-value="drawer"
:rail="rail"
permanent
app
@update:model-value="emit('update:drawer', $event)"
>
<v-list density="compact" nav>
<template v-for="item in items" :key="item.name">
<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"
open-on-hover
location="end"
:text="item.name"
>
<template #activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.name"
:to="item.path"
link
></v-list-item>
</template>
</v-tooltip>
</template>
</v-list>
</v-navigation-drawer>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
interface NavItem {
id: number;
name: string;
path: string;
icon: string;
children?: NavItem[];
}
const props = defineProps({
items: {
type: Array as () => NavItem[],
required: true,
},
rail: {
type: Boolean,
required: true,
},
drawer: {
type: Boolean,
required: true,
},
});
const emit = defineEmits(['update:drawer']);
</script>
<style scoped>
.v-navigation-drawer__content {
background-color: #ffffff;
}
</style>