Files
antrean-operasi/pages/setting/hak-akses/form.vue
T

648 lines
24 KiB
Vue

<script setup lang="ts">
import { getListRoleById, updateRolePage } from '~/services/access';
import type { PageMode } from '~/types/common';
import LoadingState from '~/components/shared/LoadingState.vue';
interface MenuItem {
title?: string;
icon?: string;
to?: string;
children?: MenuItem[] | null;
}
interface MenuGroup {
id: string;
header: string;
children: MenuItem[];
}
type CrudAction = 'create' | 'read' | 'update' | 'delete';
interface PagePermissionState {
create: boolean;
read: boolean;
update: boolean;
delete: boolean;
}
interface ApiPageItem {
id: string;
page: string;
to: string;
is_active: boolean;
sort: number;
parent_id: string;
create?: boolean;
read?: boolean;
update?: boolean;
delete?: boolean;
permissions?: CrudAction[];
}
const route = useRoute();
const router = useRouter();
// Detect mode from route query (only edit and view)
const mode = computed<PageMode>(() => {
const modeParam = route.query.mode as string;
if (modeParam === 'view') return 'view';
return 'edit'; // Default to edit
});
// Computed readonly state
const isReadonly = computed(() => mode.value === 'view');
// Dynamic page title based on mode
const pageTitle = computed(() => {
return mode.value === 'view' ? 'Detail Hak Akses' : 'Edit Hak Akses';
});
// Dynamic breadcrumbs based on mode
const breadcrumbText = computed(() => {
return mode.value === 'view' ? 'Detail' : 'Edit';
});
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(true);
const selectedPages = ref<string[]>([]);
const selectedPagePermissions = ref<Record<string, PagePermissionState>>({});
const loading = ref(false);
const currentId = ref<string | null>(null);
const menuGroups = ref<MenuGroup[]>([]);
const apiMenuItems = ref<{ id: string; header: string; pages: ApiPageItem[] }[]>([]);
// 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'
};
const crudActions: CrudAction[] = ['create', 'read', 'update', 'delete'];
const crudLabels: Record<CrudAction, string> = {
create: 'Create',
read: 'Read',
update: 'Update',
delete: 'Delete'
};
const createDefaultPagePermissions = (): PagePermissionState => ({
create: false,
read: false,
update: false,
delete: false
});
const ensurePagePermissions = (pageTo: string) => {
if (!selectedPagePermissions.value[pageTo]) {
selectedPagePermissions.value[pageTo] = createDefaultPagePermissions();
}
return selectedPagePermissions.value[pageTo];
};
const hasAnyPagePermission = (permissions: PagePermissionState) => {
return permissions.create || permissions.read || permissions.update || permissions.delete;
};
const isPageSelected = (pageTo: string) => selectedPages.value.includes(pageTo);
const syncPageSelection = (pageTo: string, checked: boolean) => {
const permissions = ensurePagePermissions(pageTo);
if (checked) {
if (!selectedPages.value.includes(pageTo)) {
selectedPages.value.push(pageTo);
}
permissions.read = true;
return;
}
selectedPages.value = selectedPages.value.filter((page) => page !== pageTo);
selectedPagePermissions.value[pageTo] = createDefaultPagePermissions();
};
const setPagePermission = (pageTo: string, action: CrudAction, checked: boolean) => {
const permissions = ensurePagePermissions(pageTo);
permissions[action] = checked;
if (checked) {
if (!selectedPages.value.includes(pageTo)) {
selectedPages.value.push(pageTo);
}
if (action !== 'read') {
permissions.read = true;
}
}
if (action === 'read' && !checked) {
permissions.create = false;
permissions.update = false;
permissions.delete = false;
}
if (hasAnyPagePermission(permissions)) {
if (!selectedPages.value.includes(pageTo)) {
selectedPages.value.push(pageTo);
}
} else {
selectedPages.value = selectedPages.value.filter((page) => page !== pageTo);
}
};
// Get all menu items with their pages
const menuItems = computed(() => {
if (apiMenuItems.value.length === 0) {
return [];
}
return apiMenuItems.value.map(section => ({
header: section.header,
pages: section.pages.map(page => ({
title: page.page,
to: page.to
}))
}));
});
// 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 => {
syncPageSelection(page.to, false);
});
} else {
// Select all in this section
sectionPages.forEach(page => {
syncPageSelection(page.to, true);
});
}
};
// 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;
}
if (!currentId.value) {
showSnackbar('ID tidak ditemukan', 'error');
return;
}
loading.value = true;
try {
// Build access_page array from API menu structure
const accessPages: any[] = [];
apiMenuItems.value.forEach((section) => {
// Check if any child in this section is selected
const hasActiveChild = section.pages.some((page) =>
selectedPages.value.includes(page.to)
);
// Add header item (parent) - active if any child is active
accessPages.push({
id: section.id,
is_active: hasActiveChild,
page: section.header,
parent_id: '',
sort: accessPages.length
});
// Add child pages
section.pages.forEach((page) => {
const permissions = selectedPagePermissions.value[page.to] ?? createDefaultPagePermissions();
const isSelected = selectedPages.value.includes(page.to) || hasAnyPagePermission(permissions);
accessPages.push({
id: page.id,
is_active: isSelected,
page: page.page,
parent_id: page.parent_id,
sort: page.sort,
access: {
create: permissions.create,
read: permissions.read || isSelected,
update: permissions.update,
delete: permissions.delete
}
});
});
});
const payload = {
id: currentId.value || '',
name: namaHakAkses.value,
status: status.value,
access_page: accessPages
};
let response;
// Update existing role
response = await updateRolePage(currentId.value, 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 = false;
selectedPages.value = [];
selectedPagePermissions.value = {};
}
};
const handleKembali = () => {
router.push('/setting/hak-akses');
};
// Helper function to transform access_page array to menu structure
const transformAccessPageToMenuStructure = (accessPages: any[]) => {
const headers = accessPages.filter((item: any) => !item.parent_id);
const children = accessPages.filter((item: any) => item.parent_id);
const menuStructure: { id: string; header: string; pages: any[] }[] = [];
headers.forEach((header: any) => {
const headerChildren = children.filter((child: any) => child.parent_id === header.id);
if (headerChildren.length > 0) {
// Add 'to' property to each child page
const pagesWithRoutes = headerChildren.map((child: any) => ({
...child,
to: child.to || `/route-placeholder/${child.page.toLowerCase().replace(/\s+/g, '-')}`
}));
menuStructure.push({
id: header.id,
header: header.page,
pages: pagesWithRoutes
});
}
});
return menuStructure;
};
// Load role data
onMounted(async () => {
loading.value = true;
try {
const id = route.query.id as string;
if (!id) {
showSnackbar('ID tidak ditemukan', 'error');
setTimeout(() => {
router.push('/setting/hak-akses');
}, 2000);
return;
}
const response = await getListRoleById(id);
if (response && response.success && response.data) {
const data = response.data;
// Store current role ID
currentId.value = id;
// Extract role name and status
if (data.name) {
namaHakAkses.value = data.name;
}
if ('status' in data) {
status.value = data.status;
}
// Extract and transform access_page array
if (data.access_page && Array.isArray(data.access_page)) {
const menuStructure = transformAccessPageToMenuStructure(data.access_page);
apiMenuItems.value = menuStructure;
selectedPagePermissions.value = {};
// Extract active pages
const extractedPages: string[] = [];
menuStructure.forEach(section => {
section.pages.forEach((page: any) => {
const permissionState = createDefaultPagePermissions();
const permissionList = page.access ? page.access : [];
permissionState.create = permissionList.create;
permissionState.read = permissionList.read;
permissionState.update = permissionList.update;
permissionState.delete = permissionList.delete;
if (permissionState.create || permissionState.update || permissionState.delete) {
permissionState.read = true;
}
selectedPagePermissions.value[page.to] = permissionState;
if (page.is_active && page.to) {
extractedPages.push(page.to);
}
if (hasAnyPagePermission(permissionState) && page.to && !extractedPages.includes(page.to)) {
extractedPages.push(page.to);
}
});
});
selectedPages.value = extractedPages;
console.log('Menu structure loaded:', menuStructure);
}
} else {
showSnackbar(response?.message || 'Data tidak ditemukan', 'error');
setTimeout(() => {
router.push('/setting/hak-akses');
}, 2000);
}
} catch (error) {
console.error('Error loading data:', error);
showSnackbar('Gagal memuat data', 'error');
setTimeout(() => {
router.push('/setting/hak-akses');
}, 2000);
} finally {
loading.value = false;
}
});
</script>
<template>
<LoadingState
:loading="loading"
loadingText="Memuat data..."
minHeight="400px"
>
<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"
color="success"
hide-details="auto"
:readonly="isReadonly"
inset
>
<template #label>
<span :class="status ? 'text-success' : 'text-error'">
{{ status ? '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"
:readonly="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 pr-3 pb-2">
<div
v-for="page in section.pages"
:key="page.to"
class="py-1"
>
<div class="d-flex flex-column gap-1">
<v-checkbox
:model-value="isPageSelected(page.to)"
@update:model-value="(value) => syncPageSelection(page.to, !!value)"
:label="page.title"
hide-details
density="compact"
color="primary"
:readonly="isReadonly"
></v-checkbox>
<div class="d-flex flex-wrap gap-2 pl-8">
<v-checkbox
v-for="action in crudActions"
:key="`${page.to}-${action}`"
:model-value="selectedPagePermissions[page.to]?.[action] ?? false"
@update:model-value="(value) => setPagePermission(page.to, action, !!value)"
:label="crudLabels[action]"
hide-details
density="compact"
color="primary"
:readonly="isReadonly"
></v-checkbox>
</div>
</div>
</div>
</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
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>
</LoadingState>
<!-- 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>