257 lines
10 KiB
Vue
257 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { Icon } from '@iconify/vue';
|
|
|
|
interface Halaman {
|
|
id: number;
|
|
name: string;
|
|
url: string;
|
|
level: number;
|
|
parent?: number;
|
|
icon?: string;
|
|
role: string[];
|
|
}
|
|
|
|
const page = ref({ title: 'Halaman' });
|
|
const breadcrumbs = ref([
|
|
{
|
|
text: 'Setting',
|
|
disabled: false,
|
|
href: '#'
|
|
},
|
|
{
|
|
text: 'Halaman',
|
|
disabled: true,
|
|
href: '#'
|
|
}
|
|
]);
|
|
|
|
const router = useRouter();
|
|
const search = ref('');
|
|
const halamanList = ref<Halaman[]>([]);
|
|
const loading = ref(false);
|
|
const expandedPanels = ref<number[]>([]);
|
|
|
|
// Snackbar state
|
|
const snackbar = ref(false);
|
|
const snackbarMessage = ref('');
|
|
const snackbarColor = ref('success');
|
|
|
|
const showSnackbar = (message: string, color: string = 'success') => {
|
|
snackbarMessage.value = message;
|
|
snackbarColor.value = color;
|
|
snackbar.value = true;
|
|
};
|
|
|
|
// Load data from API
|
|
const loadData = async () => {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const response = await $fetch('/api/halaman');
|
|
|
|
if (response && typeof response === 'object' && 'success' in response && response.success && 'data' in response) {
|
|
halamanList.value = Array.isArray(response.data) ? response.data : [];
|
|
|
|
// Expand all panels by default
|
|
const level1Count = halamanList.value.filter(h => h.level === 1).length;
|
|
expandedPanels.value = Array.from({ length: level1Count }, (_, i) => i);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading halaman:', error);
|
|
showSnackbar('Gagal memuat data', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Group pages by parent (level 1)
|
|
const groupedPages = computed(() => {
|
|
const level1 = halamanList.value.filter(h => h.level === 1);
|
|
const level2 = halamanList.value.filter(h => h.level === 2);
|
|
|
|
return level1.map(parent => ({
|
|
...parent,
|
|
children: level2.filter(child => child.parent === parent.id)
|
|
}));
|
|
});
|
|
|
|
// Filter grouped pages based on search
|
|
const filteredGroupedPages = computed(() => {
|
|
if (!search.value) return groupedPages.value;
|
|
|
|
const searchLower = search.value.toLowerCase();
|
|
return groupedPages.value
|
|
.map(group => ({
|
|
...group,
|
|
children: group.children.filter(child =>
|
|
child.name.toLowerCase().includes(searchLower) ||
|
|
child.url.toLowerCase().includes(searchLower)
|
|
)
|
|
}))
|
|
.filter(group =>
|
|
group.name.toLowerCase().includes(searchLower) ||
|
|
group.children.length > 0
|
|
);
|
|
});
|
|
|
|
const handleEdit = (halaman: Halaman) => {
|
|
router.push({
|
|
path: '/setting/halaman/form',
|
|
query: {
|
|
id: halaman.id
|
|
}
|
|
});
|
|
};
|
|
|
|
// Load data on mount
|
|
onMounted(() => {
|
|
loadData();
|
|
});
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
pageTitle: 'Halaman',
|
|
});
|
|
</script>
|
|
<template>
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<v-card elevation="10">
|
|
<v-card-text>
|
|
<div class="d-flex justify-space-between align-center mb-4">
|
|
<v-text-field
|
|
v-model="search"
|
|
placeholder="Cari halaman..."
|
|
variant="outlined"
|
|
density="comfortable"
|
|
hide-details
|
|
prepend-inner-icon="mdi-magnify"
|
|
style="max-width: 400px;"
|
|
></v-text-field>
|
|
</div>
|
|
|
|
<div v-if="loading" class="d-flex justify-center align-center pa-8">
|
|
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<v-expansion-panels
|
|
v-model="expandedPanels"
|
|
multiple
|
|
>
|
|
<v-expansion-panel
|
|
v-for="group in filteredGroupedPages"
|
|
:key="group.id"
|
|
elevation="0"
|
|
>
|
|
<v-expansion-panel-title class="bg-light-primary">
|
|
<div class="d-flex align-center ga-3">
|
|
<v-avatar size="32" class="rounded-md bg-primary">
|
|
<Icon icon="solar:folder-with-files-bold-duotone" class="text-white" height="18" />
|
|
</v-avatar>
|
|
<div>
|
|
<h6 class="text-h6 font-weight-bold">{{ group.name }}</h6>
|
|
<span class="text-caption text-medium-emphasis">{{ group.children.length }} halaman</span>
|
|
</div>
|
|
</div>
|
|
</v-expansion-panel-title>
|
|
|
|
<v-expansion-panel-text>
|
|
<v-list density="compact" class="pa-0">
|
|
<v-list-item
|
|
v-for="(child, idx) in group.children"
|
|
:key="child.id"
|
|
class="px-4 py-3"
|
|
:class="{ 'border-t': idx > 0 }"
|
|
>
|
|
<template #prepend>
|
|
<v-avatar size="28" class="rounded-md bg-lightsecondary mr-3">
|
|
<Icon
|
|
:icon="`solar:${child.icon || 'document-text-bold-duotone'}`"
|
|
class="text-secondary"
|
|
height="16"
|
|
/>
|
|
</v-avatar>
|
|
</template>
|
|
|
|
<v-list-item-title class="font-weight-medium">
|
|
{{ child.name }}
|
|
</v-list-item-title>
|
|
<v-list-item-subtitle class="text-caption">
|
|
{{ child.url }}
|
|
</v-list-item-subtitle>
|
|
|
|
<template #append>
|
|
<div class="d-flex align-center ga-2">
|
|
<div v-if="child.role.length > 0" class="d-flex ga-1 mr-2">
|
|
<v-chip
|
|
v-for="role in child.role"
|
|
:key="role"
|
|
size="small"
|
|
color="primary"
|
|
variant="tonal"
|
|
>
|
|
{{ role }}
|
|
</v-chip>
|
|
</div>
|
|
<v-chip
|
|
v-else
|
|
size="small"
|
|
color="error"
|
|
variant="tonal"
|
|
class="mr-2"
|
|
>
|
|
Belum ada role
|
|
</v-chip>
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
color="primary"
|
|
@click="handleEdit(child)"
|
|
>
|
|
<v-icon size="small">mdi-account-key-outline</v-icon>
|
|
<v-tooltip activator="parent" location="top">Assign Role</v-tooltip>
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
</v-expansion-panels>
|
|
|
|
<v-alert
|
|
v-if="filteredGroupedPages.length === 0"
|
|
type="info"
|
|
variant="tonal"
|
|
class="mt-4"
|
|
>
|
|
Tidak ada data halaman yang ditemukan
|
|
</v-alert>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Snackbar for notifications -->
|
|
<v-snackbar
|
|
v-model="snackbar"
|
|
:color="snackbarColor"
|
|
:timeout="3000"
|
|
location="top"
|
|
>
|
|
{{ snackbarMessage }}
|
|
<template #actions>
|
|
<v-btn
|
|
variant="text"
|
|
@click="snackbar = false"
|
|
>
|
|
Close
|
|
</v-btn>
|
|
</template>
|
|
</v-snackbar>
|
|
</template> |