Files
ntspi-app/resources/js/Pages/Dashboard/Roles/Edit.vue
T
F4ilji 63e456088d feat(dashboard): add Roles CRUD with permission-based menu filtering
- Roles CRUD: List/Create/Update/Delete actions, controller, requests
- Roles Vue pages: Index, Create, Edit (Options API, DashboardLayout)
- SidebarNav: filter menu items by user permissions via Inertia shared data
- HandleInertiaRequests: share permissions and roles arrays to frontend
- HttpKernel: register dashboard.permission middleware alias
- SidebarNavItem: add shield-check icon mapping
2026-08-03 10:39:15 +05:00

180 lines
6.9 KiB
Vue

<template>
<DashboardLayout>
<template #header-icon>
<DashboardIcon name="pencil-square" size="5" class="text-primary" />
</template>
<template #header-title>Редактирование роли</template>
<template #header-subtitle>Изменение параметров роли «{{ role.name }}»</template>
<template #header-actions>
<a
:href="route('dashboard.roles.index')"
class="inline-flex items-center gap-2 px-4 py-2 bg-surface text-foreground text-sm font-medium rounded-lg border border-layer-line hover:bg-muted-hover transition-all duration-200"
>
<DashboardIcon name="arrow-left" size="4" />
Назад к списку
</a>
</template>
<FlashMessages />
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
<form @submit.prevent="submit">
<div class="p-6 space-y-6">
<!-- Name -->
<div>
<label class="block text-sm font-medium text-foreground mb-1">
Название роли <span class="text-danger">*</span>
</label>
<input
v-model="form.name"
type="text"
required
maxlength="255"
placeholder="Например: editor, viewer, admin"
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<p v-if="errors.name" class="mt-1 text-sm text-danger">{{ errors.name }}</p>
</div>
<!-- Permissions -->
<div>
<h3 class="text-lg font-medium text-foreground mb-3">Пермишены</h3>
<p class="text-sm text-muted-foreground-1 mb-4">Выберите пермишены для этой роли</p>
<div v-for="(group, prefix) in groupedPermissions" :key="prefix" class="mb-4">
<div
class="flex items-center justify-between px-4 py-2.5 bg-surface/50 border border-layer-line rounded-t-lg cursor-pointer hover:bg-muted-hover transition-colors"
@click="toggleGroup(prefix)"
>
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isGroupSelected(prefix)"
@change.stop="toggleGroup(prefix)"
class="w-4 h-4 rounded border-layer-line text-primary focus:ring-primary focus:ring-offset-0"
/>
<span class="text-sm font-medium text-foreground">{{ prefixLabels[prefix] || prefix }}</span>
<span class="text-xs text-muted-foreground-1">({{ group.length }})</span>
</div>
</div>
<div class="border border-t-0 border-layer-line rounded-b-lg px-4 py-3 bg-white">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
<label
v-for="perm in group"
:key="perm.name"
class="flex items-center gap-2 cursor-pointer hover:bg-muted-hover/50 px-2 py-1 rounded transition-colors"
>
<input
type="checkbox"
:value="perm.name"
v-model="form.permissions"
class="w-4 h-4 rounded border-layer-line text-primary focus:ring-primary focus:ring-offset-0"
/>
<span class="text-sm text-foreground">{{ perm.name }}</span>
</label>
</div>
</div>
</div>
<p v-if="errors.permissions" class="mt-1 text-sm text-danger">{{ errors.permissions }}</p>
</div>
</div>
<!-- Actions -->
<div class="px-6 py-4 bg-surface/50 border-t border-layer-line flex items-center justify-end gap-3">
<a
:href="route('dashboard.roles.index')"
class="px-4 py-2 text-sm font-medium text-foreground bg-surface border border-layer-line rounded-lg hover:bg-muted-hover transition-all"
>
Отмена
</a>
<button
type="submit"
:disabled="processing"
class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ processing ? 'Сохранение...' : 'Сохранить изменения' }}
</button>
</div>
</form>
</div>
</DashboardLayout>
</template>
<script setup>
import { reactive, computed, onMounted } from 'vue';
import { router } from '@inertiajs/vue3';
import DashboardLayout from '../Components/DashboardLayout.vue';
import DashboardIcon from '../Components/DashboardIcon.vue';
import FlashMessages from '../Components/shared/FlashMessages.vue';
const props = defineProps({
role: { type: Object, required: true },
permissions: { type: Array, required: true },
errors: { type: Object, default: () => ({}) },
});
const form = reactive({
name: props.role.name,
permissions: [...props.role.permissions],
});
const processing = false;
const prefixLabels = {
view_any: 'Просмотр',
create: 'Создание',
update: 'Редактирование',
delete: 'Удаление',
restore: 'Восстановление',
force_delete: 'Принудительное удаление',
replicate: 'Дублирование',
reorder: 'Переупорядочивание',
};
const groupedPermissions = computed(() => {
const groups = {};
for (const perm of props.permissions) {
const parts = perm.name.split('_');
let prefix;
if (parts[0] === 'view' && parts[1] === 'any') {
prefix = 'view_any';
} else if (parts[0] === 'force' && parts[1] === 'delete') {
prefix = 'force_delete';
} else {
prefix = parts[0];
}
if (!groups[prefix]) groups[prefix] = [];
groups[prefix].push(perm);
}
return groups;
});
const isGroupSelected = (prefix) => {
const group = groupedPermissions.value[prefix] || [];
return group.length > 0 && group.every(p => form.permissions.includes(p.name));
};
const toggleGroup = (prefix) => {
const group = groupedPermissions.value[prefix] || [];
if (isGroupSelected(prefix)) {
const groupNames = group.map(p => p.name);
form.permissions = form.permissions.filter(name => !groupNames.includes(name));
} else {
const groupNames = group.map(p => p.name);
form.permissions = [...new Set([...form.permissions, ...groupNames])];
}
};
const submit = () => {
router.put(route('dashboard.roles.update', props.role.id), {
name: form.name,
permissions: form.permissions,
});
};
onMounted(() => {
document.title = 'Редактирование роли — Dashboard';
});
</script>