440 lines
16 KiB
Vue
440 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import sidebarItem from '@/components/layout/full/vertical-sidebar/sidebarItem';
|
|
import type { PageMode } from '~/types/common';
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
// Detect mode from route query
|
|
const mode = computed<PageMode>(() => {
|
|
const modeParam = route.query.mode as string;
|
|
if (modeParam === 'edit') return 'edit';
|
|
if (modeParam === 'view') return 'view';
|
|
return 'create';
|
|
});
|
|
|
|
// Computed readonly state
|
|
const isReadonly = computed(() => mode.value === 'view');
|
|
|
|
// Dynamic page title based on mode
|
|
const pageTitle = computed(() => {
|
|
switch (mode.value) {
|
|
case 'edit':
|
|
return 'Edit Hak Akses';
|
|
case 'view':
|
|
return 'Detail Hak Akses';
|
|
default:
|
|
return 'Tambah Hak Akses';
|
|
}
|
|
});
|
|
|
|
// Dynamic breadcrumbs based on mode
|
|
const breadcrumbText = computed(() => {
|
|
switch (mode.value) {
|
|
case 'edit':
|
|
return 'Edit';
|
|
case 'view':
|
|
return 'Detail';
|
|
default:
|
|
return 'Tambah';
|
|
}
|
|
});
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
buttonBack: true,
|
|
pageTitle: '',
|
|
breadcrumbs: [
|
|
{ text: 'Setting' },
|
|
{ text: 'Hak Akses' },
|
|
{ text: '' }
|
|
]
|
|
});
|
|
|
|
// Update page meta dynamically
|
|
watch([pageTitle, breadcrumbText], ([title, crumb]) => {
|
|
if (process.client) {
|
|
route.meta.pageTitle = title;
|
|
if (route.meta.breadcrumbs && Array.isArray(route.meta.breadcrumbs)) {
|
|
route.meta.breadcrumbs[2] = { text: crumb };
|
|
}
|
|
}
|
|
}, { immediate: true });
|
|
|
|
// Form data
|
|
const form = ref();
|
|
const valid = ref(true);
|
|
const namaHakAkses = ref('');
|
|
const status = ref('aktif');
|
|
const selectedPages = ref<string[]>([]);
|
|
const loading = ref(false);
|
|
const currentId = ref<number | null>(null);
|
|
|
|
// 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;
|
|
};
|
|
|
|
// Rules
|
|
const rules = {
|
|
required: (value: string) => !!value || 'Field ini wajib diisi'
|
|
};
|
|
|
|
// Get all menu items with their pages
|
|
const menuItems = computed(() => {
|
|
const items: { header: string; pages: { title: string; to: string }[] }[] = [];
|
|
|
|
sidebarItem.forEach(section => {
|
|
if (section.children && section.children.length > 0) {
|
|
items.push({
|
|
header: section.header || '',
|
|
pages: section.children.map(child => ({
|
|
title: child.title || '',
|
|
to: child.to || ''
|
|
}))
|
|
});
|
|
}
|
|
});
|
|
|
|
return items;
|
|
});
|
|
|
|
// Toggle all pages in a section
|
|
const toggleSection = (sectionPages: { to: string }[]) => {
|
|
const allSelected = sectionPages.every(page => selectedPages.value.includes(page.to));
|
|
|
|
if (allSelected) {
|
|
// Unselect all in this section
|
|
sectionPages.forEach(page => {
|
|
const index = selectedPages.value.indexOf(page.to);
|
|
if (index > -1) {
|
|
selectedPages.value.splice(index, 1);
|
|
}
|
|
});
|
|
} else {
|
|
// Select all in this section
|
|
sectionPages.forEach(page => {
|
|
if (!selectedPages.value.includes(page.to)) {
|
|
selectedPages.value.push(page.to);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Check if all pages in section are selected
|
|
const isSectionSelected = (sectionPages: { to: string }[]) => {
|
|
return sectionPages.length > 0 && sectionPages.every(page => selectedPages.value.includes(page.to));
|
|
};
|
|
|
|
// Check if some pages in section are selected
|
|
const isSectionIndeterminate = (sectionPages: { to: string }[]) => {
|
|
const selectedCount = sectionPages.filter(page => selectedPages.value.includes(page.to)).length;
|
|
return selectedCount > 0 && selectedCount < sectionPages.length;
|
|
};
|
|
|
|
const handleSimpan = async () => {
|
|
const { valid } = await form.value.validate();
|
|
|
|
if (valid) {
|
|
if (selectedPages.value.length === 0) {
|
|
showSnackbar('Pilih minimal satu halaman untuk hak akses', 'error');
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
try {
|
|
const payload = {
|
|
namaHakAkses: namaHakAkses.value,
|
|
status: status.value,
|
|
pages: selectedPages.value
|
|
};
|
|
|
|
let response;
|
|
|
|
if (mode.value === 'edit' && currentId.value) {
|
|
// Update existing
|
|
response = await $fetch(`/api/hak-akses/${currentId.value}`, {
|
|
method: 'PUT',
|
|
body: payload
|
|
});
|
|
} else {
|
|
// Create new
|
|
response = await $fetch('/api/hak-akses', {
|
|
method: 'POST',
|
|
body: payload
|
|
});
|
|
}
|
|
|
|
if (response && typeof response === 'object' && 'success' in response && response.success) {
|
|
const message = 'message' in response && typeof response.message === 'string'
|
|
? response.message
|
|
: 'Data berhasil disimpan';
|
|
showSnackbar(message, 'success');
|
|
|
|
// Redirect to list after 1 second
|
|
setTimeout(() => {
|
|
router.push('/setting/hak-akses');
|
|
}, 1000);
|
|
} else {
|
|
const message = response && typeof response === 'object' && 'message' in response && typeof response.message === 'string'
|
|
? response.message
|
|
: 'Gagal menyimpan data';
|
|
showSnackbar(message, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving hak akses:', error);
|
|
showSnackbar('Terjadi kesalahan saat menyimpan data', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (confirm('Apakah Anda yakin ingin mereset form?')) {
|
|
form.value.reset();
|
|
namaHakAkses.value = '';
|
|
status.value = 'aktif';
|
|
selectedPages.value = [];
|
|
}
|
|
};
|
|
|
|
const handleKembali = () => {
|
|
router.push('/setting/hak-akses');
|
|
};
|
|
|
|
// Load data if edit or view mode
|
|
onMounted(async () => {
|
|
if (mode.value !== 'create') {
|
|
const id = route.query.id;
|
|
|
|
if (id) {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const response = await $fetch(`/api/hak-akses/${id}`);
|
|
|
|
if (response && typeof response === 'object' && 'success' in response && response.success && 'data' in response) {
|
|
const data = response.data;
|
|
|
|
if (data && typeof data === 'object') {
|
|
currentId.value = 'id' in data && typeof data.id === 'number' ? data.id : null;
|
|
namaHakAkses.value = 'namaHakAkses' in data && typeof data.namaHakAkses === 'string' ? data.namaHakAkses : '';
|
|
status.value = 'status' in data && typeof data.status === 'string' ? data.status : 'aktif';
|
|
selectedPages.value = 'pages' in data && Array.isArray(data.pages) ? data.pages : [];
|
|
}
|
|
} else {
|
|
const message = response && typeof response === 'object' && 'message' in response && typeof response.message === 'string'
|
|
? response.message
|
|
: 'Data tidak ditemukan';
|
|
showSnackbar(message, 'error');
|
|
|
|
// Redirect to list after 2 seconds
|
|
setTimeout(() => {
|
|
router.push('/setting/hak-akses');
|
|
}, 2000);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading hak akses:', error);
|
|
showSnackbar('Gagal memuat data', 'error');
|
|
|
|
// Redirect to list after 2 seconds
|
|
setTimeout(() => {
|
|
router.push('/setting/hak-akses');
|
|
}, 2000);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<v-form ref="form" v-model="valid" lazy-validation>
|
|
<v-row>
|
|
<!-- Form Card -->
|
|
<v-col cols="12">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<h5 class="text-h5 font-weight-bold">Informasi Hak Akses</h5>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<v-row>
|
|
<!-- Nama Hak Akses -->
|
|
<v-col cols="12" md="6">
|
|
<v-label class="mb-2 font-weight-medium">Nama Hak Akses <span class="text-error">*</span></v-label>
|
|
<v-text-field
|
|
v-model="namaHakAkses"
|
|
placeholder="Contoh: Admin, Dokter, Perawat"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
:rules="[rules.required]"
|
|
hide-details="auto"
|
|
:readonly="isReadonly"
|
|
:bg-color="isReadonly ? 'grey-lighten-3' : undefined"
|
|
></v-text-field>
|
|
</v-col>
|
|
|
|
<!-- Status -->
|
|
<v-col cols="12" md="6">
|
|
<v-label class="mb-2 font-weight-medium">Status <span class="text-error">*</span></v-label>
|
|
<v-switch
|
|
v-model="status"
|
|
true-value="aktif"
|
|
false-value="tidak aktif"
|
|
color="success"
|
|
hide-details="auto"
|
|
:readonly="isReadonly"
|
|
:disabled="isReadonly"
|
|
inset
|
|
>
|
|
<template #label>
|
|
<span :class="status === 'aktif' ? 'text-success' : 'text-error'">
|
|
{{ status === 'aktif' ? 'Aktif' : 'Tidak Aktif' }}
|
|
</span>
|
|
</template>
|
|
</v-switch>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Akses Halaman Card -->
|
|
<v-col cols="12">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<h5 class="text-h5 font-weight-bold">Akses Halaman</h5>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<v-row>
|
|
<v-col
|
|
v-for="(section, index) in menuItems"
|
|
:key="index"
|
|
cols="12"
|
|
md="4"
|
|
>
|
|
<v-card variant="outlined">
|
|
<div class="d-flex align-center pa-1">
|
|
<v-checkbox
|
|
:model-value="isSectionSelected(section.pages)"
|
|
:indeterminate="isSectionIndeterminate(section.pages)"
|
|
@click="!isReadonly && toggleSection(section.pages)"
|
|
hide-details
|
|
density="compact"
|
|
color="primary"
|
|
:disabled="isReadonly"
|
|
>
|
|
<template #label>
|
|
<span class="text-capitalize font-weight-bold">
|
|
{{ section.header }}
|
|
</span>
|
|
</template>
|
|
</v-checkbox>
|
|
</div>
|
|
|
|
<v-divider class="mb-1"></v-divider>
|
|
|
|
<div class="pl-3">
|
|
<v-checkbox
|
|
v-for="page in section.pages"
|
|
:key="page.to"
|
|
v-model="selectedPages"
|
|
:value="page.to"
|
|
:label="page.title"
|
|
hide-details
|
|
density="compact"
|
|
color="primary"
|
|
:disabled="isReadonly"
|
|
></v-checkbox>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Selected Pages Summary -->
|
|
<v-row v-if="selectedPages.length > 0" class="mt-4">
|
|
<v-col cols="12">
|
|
<v-alert type="info" variant="tonal" density="compact">
|
|
<div class="d-flex align-center">
|
|
<span><strong>{{ selectedPages.length }}</strong> halaman dipilih</span>
|
|
</div>
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Action Buttons -->
|
|
<v-col cols="12">
|
|
<v-card elevation="10">
|
|
<v-card-text>
|
|
<div class="d-flex justify-end ga-3">
|
|
<v-btn
|
|
color="default"
|
|
size="large"
|
|
variant="outlined"
|
|
prepend-icon="mdi-arrow-left"
|
|
@click="handleKembali"
|
|
>
|
|
Kembali
|
|
</v-btn>
|
|
<v-btn
|
|
v-if="!isReadonly"
|
|
color="error"
|
|
size="large"
|
|
variant="outlined"
|
|
prepend-icon="mdi-reload"
|
|
@click="handleReset"
|
|
>
|
|
Reset
|
|
</v-btn>
|
|
<v-btn
|
|
v-if="!isReadonly"
|
|
color="primary"
|
|
size="large"
|
|
prepend-icon="mdi-content-save"
|
|
@click="handleSimpan"
|
|
:loading="loading"
|
|
:disabled="loading"
|
|
>
|
|
Simpan
|
|
</v-btn>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
|
|
<!-- 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> |