add permission role
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import api from '@/utils/api';
|
||||
import AppRightDialog from '~/components/shared/AppRightDialog.vue';
|
||||
import { useSnackbarStore } from '~/store/snackbar';
|
||||
import type {ModalMode,ParentPageOption} from '~/types/setting/menu';
|
||||
import { useValidation } from '~/composables/useValidation';
|
||||
@@ -65,7 +66,7 @@ const titleText = computed(() => {
|
||||
});
|
||||
|
||||
|
||||
const submitLabel = computed(() => (isEditMode.value ? 'Update' : 'Save'));
|
||||
const submitLabel = computed(() => (isEditMode.value ? 'Update' : 'Simpan'));
|
||||
|
||||
const resetForm = () => {
|
||||
entryType.value = 'main';
|
||||
@@ -259,16 +260,29 @@ const close = () => {
|
||||
lastLoadedKey = '';
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetCurrent = async () => {
|
||||
if (isDetailMode.value) return;
|
||||
if (isSaving.value || isLoadingDetail.value) return;
|
||||
|
||||
if (isCreateMode.value) {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// For edit mode, restore to the last saved/loaded state
|
||||
lastLoadedKey = '';
|
||||
await fetchPageDetail();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600px" persistent>
|
||||
<AppRightDialog v-model="dialog" :max-width="500" persistent>
|
||||
<v-card>
|
||||
<v-form v-model="valid" ref="formRef" @submit.prevent="save">
|
||||
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-3">
|
||||
<div class="d-flex align-center">
|
||||
<span class="ml-3 text-h6">{{ titleText }}</span>
|
||||
<span class="ml-3 text-h5">{{ titleText }}</span>
|
||||
</div>
|
||||
<v-btn
|
||||
icon
|
||||
@@ -316,7 +330,7 @@ const close = () => {
|
||||
|
||||
<div>
|
||||
<label class="font-weight-medium required">URL</label>
|
||||
<v-text-field v-model="url" class="mt-2" placeholder="/path/to/page" variant="outlined"
|
||||
<v-text-field v-model="url" class="mt-2" placeholder="" variant="outlined"
|
||||
density="compact" :readonly="isFormDisabled" :rules="[required]" />
|
||||
</div>
|
||||
|
||||
@@ -350,10 +364,36 @@ const close = () => {
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="!isDetailMode" color="primary" type="submit" variant="flat" :loading="isSaving" :disabled="isSaving">{{ submitLabel }}</v-btn>
|
||||
<v-row v-if="!isDetailMode" class="w-100" dense>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
prepend-icon="mdi-refresh"
|
||||
class="w-100"
|
||||
type="button"
|
||||
color="default"
|
||||
variant="outlined"
|
||||
:disabled="isFormDisabled"
|
||||
@click="resetCurrent"
|
||||
>
|
||||
Reset
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
prepend-icon="mdi-content-save"
|
||||
class="w-100"
|
||||
color="primary"
|
||||
type="submit"
|
||||
variant="flat"
|
||||
:loading="isSaving"
|
||||
:disabled="isFormDisabled"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</AppRightDialog>
|
||||
</template>
|
||||
|
||||
@@ -24,12 +24,20 @@ const handleDelete = (page: RolePage) => emit('delete', page);
|
||||
<v-card class="mb-3 page-card" elevation="2" @click="handleDetail(page)">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar color="primary" size="40" class="me-3">
|
||||
<Icon :icon="'solar:' + page.icon" width="24" />
|
||||
</v-avatar>
|
||||
<v-tooltip location="bottom" :text="page.active ? 'Aktif' : 'Tidak Aktif'">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-avatar v-bind="activatorProps" :color="page.active ? 'primary' : 'muted'" size="40" class="me-3">
|
||||
<Icon :icon="'solar:' + page.icon" width="24" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center">
|
||||
<h6 class="text-subtitle-1 font-weight-semibold">{{ page.name }}</h6>
|
||||
<h6 class="text-subtitle-1 font-weight-semibold">{{ page.name }}
|
||||
<!-- <v-chip size="small" :color="page.active ? 'success' : 'default'" class="ml-2">
|
||||
{{ page.active ? 'Aktif' : 'Tidak Aktif' }}
|
||||
</v-chip> -->
|
||||
</h6>
|
||||
</div>
|
||||
<p class="text-muted mb-0">{{ page.url }}</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import type { RoleAccessItem } from '~/types/setting/rolePermission';
|
||||
|
||||
const props = defineProps<{
|
||||
item: RoleAccessItem;
|
||||
level: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle', payload: { id: number; key: 'read' | 'create' | 'update' | 'delete'; value: boolean }): void;
|
||||
}>();
|
||||
|
||||
const onToggle = (key: 'read' | 'create' | 'update' | 'delete', value: boolean) => {
|
||||
if (props.item.permission.disable) return;
|
||||
emit('toggle', { id: props.item.id, key, value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="{ marginLeft: (level > 0 ? 40 : 0) + 'px' }"
|
||||
:class="{ 'is-child': level > 0 }"
|
||||
class="permission-row-container"
|
||||
>
|
||||
<v-card class="permission-card mb-1" elevation="0">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center flex-grow-1">
|
||||
<v-icon v-if="level > 0" class="me-2" size="18">mdi-subdirectory-arrow-right</v-icon>
|
||||
|
||||
<v-tooltip location="bottom" :text="item.active ? 'Aktif' : 'Tidak Aktif'">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-avatar v-bind="activatorProps" :color="item.active ? 'primary' : 'muted'" size="40" class="me-3">
|
||||
<Icon v-if="item.icon" :icon="'solar:' + item.icon" width="24" />
|
||||
<v-icon v-else size="24">mdi-shield-account</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center">
|
||||
<h6 class="text-subtitle-1 font-weight-semibold">{{ item.name }}</h6>
|
||||
</div>
|
||||
<p class="text-muted mb-0">{{ item.url }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="perm-cell">
|
||||
<v-checkbox-btn
|
||||
:model-value="item.permission.read"
|
||||
density="compact"
|
||||
color="primary"
|
||||
:disabled="item.permission.disable"
|
||||
@update:modelValue="(v) => onToggle('read', Boolean(v))"
|
||||
/>
|
||||
</div>
|
||||
<div class="perm-cell">
|
||||
<v-checkbox-btn
|
||||
:model-value="item.permission.create"
|
||||
density="compact"
|
||||
color="primary"
|
||||
:disabled="item.permission.disable"
|
||||
@update:modelValue="(v) => onToggle('create', Boolean(v))"
|
||||
/>
|
||||
</div>
|
||||
<div class="perm-cell">
|
||||
<v-checkbox-btn
|
||||
:model-value="item.permission.update"
|
||||
density="compact"
|
||||
color="primary"
|
||||
:disabled="item.permission.disable"
|
||||
@update:modelValue="(v) => onToggle('update', Boolean(v))"
|
||||
/>
|
||||
</div>
|
||||
<div class="perm-cell">
|
||||
<v-checkbox-btn
|
||||
:model-value="item.permission.delete"
|
||||
density="compact"
|
||||
color="primary"
|
||||
:disabled="item.permission.disable"
|
||||
@update:modelValue="(v) => onToggle('delete', Boolean(v))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div v-if="item.children?.length" class="children-container">
|
||||
<PermissionRow
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@toggle="emit('toggle', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permission-row-container {
|
||||
position: relative;
|
||||
}
|
||||
.permission-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.perm-cell {
|
||||
width: 84px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.perm-cell .v-selection-control {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type { RoleMaster } from '~/types/setting/roleMaster';
|
||||
import { formatDateTime } from '~/utils/datetime';
|
||||
|
||||
defineProps<{
|
||||
role: RoleMaster;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['detail', 'edit-role', 'delete-role', 'assign-permission']);
|
||||
|
||||
const handleDetail = (role: RoleMaster) => emit('detail', role);
|
||||
const handleEditRole = (role: RoleMaster) => emit('edit-role', role);
|
||||
const handleDeleteRole = (role: RoleMaster) => emit('delete-role', role);
|
||||
const handleAssignPermission = (role: RoleMaster) => emit('assign-permission', role);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-row-container">
|
||||
<v-card class="mb-3 page-card" elevation="2" @click="handleDetail(role)">
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center">
|
||||
<v-tooltip location="bottom" :text="role.active ? 'Aktif' : 'Tidak Aktif'">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-avatar v-bind="activatorProps" :color="role.active ? 'primary' : 'muted'" size="40" class="me-3">
|
||||
<v-icon>mdi-shield-account</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center">
|
||||
<h6 class="text-subtitle-1 font-weight-semibold">{{ role.name }}</h6>
|
||||
</div>
|
||||
<p class="text-muted mb-0">{{ formatDateTime(role.created_at) }}</p>
|
||||
</div>
|
||||
|
||||
<v-tooltip location="bottom" text="Assign permission">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" icon variant="text" class="ml-2" @click.stop="handleAssignPermission(role)">
|
||||
<v-icon>mdi-account-cog</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="bottom" text="Edit role">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" icon variant="text" class="ml-2" @click.stop="handleEditRole(role)">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="bottom" text="Hapus role">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" icon variant="text" class="ml-2" @click.stop="handleDeleteRole(role)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-row-container {
|
||||
position: relative;
|
||||
}
|
||||
.page-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import api from '@/utils/api';
|
||||
import AppRightDialog from '~/components/shared/AppRightDialog.vue';
|
||||
import { useSnackbarStore } from '~/store/snackbar';
|
||||
import type { ModalMode } from '~/types/setting/menu';
|
||||
import type { RoleMaster, RoleMasterResponse } from '~/types/setting/roleMaster';
|
||||
import { useValidation } from '~/composables/useValidation';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
mode?: ModalMode;
|
||||
roleId?: number | null;
|
||||
}>(),
|
||||
{
|
||||
mode: 'create',
|
||||
roleId: null,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'saved'): void;
|
||||
}>();
|
||||
|
||||
const dialog = ref(props.modelValue);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
dialog.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(dialog, (newValue) => {
|
||||
if (!newValue) emit('update:modelValue', false);
|
||||
});
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const { required } = useValidation();
|
||||
|
||||
const formRef = ref<any>(null);
|
||||
const valid = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isLoadingDetail = ref(false);
|
||||
|
||||
const name = ref('');
|
||||
const active = ref(true);
|
||||
|
||||
const isDetailMode = computed(() => props.mode === 'detail');
|
||||
const isEditMode = computed(() => props.mode === 'edit');
|
||||
const isCreateMode = computed(() => props.mode === 'create');
|
||||
const isFormDisabled = computed(() => isDetailMode.value || isSaving.value || isLoadingDetail.value);
|
||||
|
||||
const titleText = computed(() => {
|
||||
if (isCreateMode.value) return 'Tambah Role';
|
||||
if (isEditMode.value) return 'Edit Role';
|
||||
return 'Detail Role';
|
||||
});
|
||||
|
||||
const submitLabel = computed(() => (isEditMode.value ? 'Update' : 'Save'));
|
||||
|
||||
const resetForm = () => {
|
||||
name.value = '';
|
||||
active.value = true;
|
||||
valid.value = false;
|
||||
formRef.value?.resetValidation?.();
|
||||
};
|
||||
|
||||
let lastLoadedKey = '';
|
||||
|
||||
const fetchRoleDetail = async () => {
|
||||
if (!props.roleId) return;
|
||||
|
||||
const key = `${props.mode}:${props.roleId}`;
|
||||
if (lastLoadedKey === key) return;
|
||||
lastLoadedKey = key;
|
||||
|
||||
isLoadingDetail.value = true;
|
||||
try {
|
||||
const response = await api.get<RoleMasterResponse>(`/api/v1/roles/master/${props.roleId}`);
|
||||
const data: RoleMaster | undefined = response.data?.data;
|
||||
if (!data) throw new Error('Data role tidak ditemukan');
|
||||
|
||||
name.value = data.name ?? '';
|
||||
active.value = typeof data.active === 'boolean' ? data.active : true;
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message || 'Gagal memuat detail role';
|
||||
snackbarStore.showSnackbar(message, 'error');
|
||||
} finally {
|
||||
isLoadingDetail.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
[dialog, () => props.mode, () => props.roleId],
|
||||
async ([isOpen, mode]) => {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (mode === 'create') {
|
||||
lastLoadedKey = '';
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchRoleDetail();
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
emit('update:modelValue', false);
|
||||
lastLoadedKey = '';
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
const run = async () => {
|
||||
if (isDetailMode.value) return;
|
||||
if (isSaving.value) return;
|
||||
isSaving.value = true;
|
||||
|
||||
try {
|
||||
await formRef.value?.validate?.();
|
||||
if (!valid.value) {
|
||||
snackbarStore.showSnackbar('Lengkapi form terlebih dahulu', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
name: name.value,
|
||||
active: active.value,
|
||||
};
|
||||
|
||||
if (isCreateMode.value) {
|
||||
await api.post('/api/v1/roles/master', payload);
|
||||
snackbarStore.showSnackbar('Role berhasil ditambahkan', 'success');
|
||||
} else if (isEditMode.value) {
|
||||
if (!props.roleId) throw new Error('ID role tidak ditemukan');
|
||||
await api.put(`/api/v1/roles/master/${props.roleId}`, payload);
|
||||
snackbarStore.showSnackbar('Role berhasil diperbarui', 'success');
|
||||
}
|
||||
|
||||
emit('saved');
|
||||
close();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.message || 'Gagal menyimpan role';
|
||||
snackbarStore.showSnackbar(message, 'error');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppRightDialog v-model="dialog" :max-width="500" persistent>
|
||||
<v-card>
|
||||
<v-form v-model="valid" ref="formRef" @submit.prevent="save">
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-3">
|
||||
<span class="ml-3 text-h6">{{ titleText }}</span>
|
||||
<v-btn icon variant="text" @click="close">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text>
|
||||
<div>
|
||||
<label class="font-weight-medium required">Nama Role</label>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
class="mt-2"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:readonly="isFormDisabled"
|
||||
:rules="[required]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-weight-medium required">Status</label>
|
||||
<v-switch v-model="active" class="mt-2" color="primary" inset :readonly="isFormDisabled" hide-details>
|
||||
<template #label>
|
||||
<span>{{ active ? 'Aktif' : 'Nonaktif' }}</span>
|
||||
</template>
|
||||
</v-switch>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn v-if="!isDetailMode" class="w-50" color="primary" type="submit" variant="flat" :loading="isSaving" :disabled="isSaving">
|
||||
{{ submitLabel }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</AppRightDialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user