Create display droit access

This commit is contained in:
kevin 2026-04-28 10:26:45 +03:00
parent d275c460b6
commit e27cce45e0
7 changed files with 1172 additions and 11 deletions

View File

@ -40,6 +40,11 @@ return [
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'sanctum' => [
'driver' => 'sanctum',
'provider' => 'users',
],
], ],
/* /*

View File

@ -0,0 +1,39 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AdminAccessSeeder extends Seeder
{
/**
* Seed the application's admin access control data.
*/
public function run(): void
{
$permission = Permission::query()->firstOrCreate([
'name' => 'config.view_roles',
'guard_name' => 'sanctum',
]);
$role = Role::query()->firstOrCreate([
'name' => 'administrator',
'guard_name' => 'sanctum',
]);
$role->givePermissionTo($permission);
$adminUser = User::query()->updateOrCreate(
['email' => 'admin@admin.com'],
[
'name' => 'Admin User',
'password' => 'password',
]
);
$adminUser->assignRole($role);
}
}

View File

@ -15,11 +15,7 @@ class DatabaseSeeder extends Seeder
{ {
// User::factory(10)->create(); // User::factory(10)->create();
User::factory()->create([ $this->call(AdminAccessSeeder::class);
'name' => 'Admin User',
'email' => 'admin@admin.com',
'password' => 'password',
]);
$this->call(ProductCategorySeeder::class); $this->call(ProductCategorySeeder::class);
$this->call(EmployeeSeeder::class); $this->call(EmployeeSeeder::class);

View File

@ -0,0 +1,151 @@
import { request } from "./http";
export interface AccessControlRole {
id: number;
name: string;
guard_name: string;
permissions?: AccessControlPermission[];
users_count?: number;
}
export interface AccessControlPermission {
id: number;
name: string;
guard_name: string;
roles?: Array<Pick<AccessControlRole, "id" | "name">>;
}
export interface AccessControlIndexResponse {
data: {
roles: AccessControlRole[];
permissions: AccessControlPermission[];
};
message: string;
}
export interface CreateRolePayload {
name: string;
guard_name?: string;
permissions?: string[];
}
export interface UpdateRolePayload extends Partial<CreateRolePayload> {
id: number;
}
export interface SyncRolePermissionsPayload {
id: number;
permissions: string[];
}
export interface CreatePermissionPayload {
name: string;
guard_name?: string;
}
export interface UpdatePermissionPayload
extends Partial<CreatePermissionPayload> {
id: number;
}
export const AccessControlService = {
async getAccessControl(): Promise<AccessControlIndexResponse["data"]> {
const response = await request<AccessControlIndexResponse>({
url: "/api/access-control",
method: "get",
});
return response.data;
},
async createRole(payload: CreateRolePayload): Promise<AccessControlRole> {
const response = await request<{
data: AccessControlRole;
message: string;
}>({
url: "/api/access-control/roles",
method: "post",
data: payload,
});
return response.data;
},
async updateRole(payload: UpdateRolePayload): Promise<AccessControlRole> {
const { id, ...data } = payload;
const response = await request<{
data: AccessControlRole;
message: string;
}>({
url: `/api/access-control/roles/${id}`,
method: "put",
data,
});
return response.data;
},
async deleteRole(id: number): Promise<{ message: string }> {
return request<{ message: string }>({
url: `/api/access-control/roles/${id}`,
method: "delete",
});
},
async syncRolePermissions(
payload: SyncRolePermissionsPayload
): Promise<AccessControlRole> {
const response = await request<{
data: AccessControlRole;
message: string;
}>({
url: `/api/access-control/roles/${payload.id}/permissions`,
method: "put",
data: { permissions: payload.permissions },
});
return response.data;
},
async createPermission(
payload: CreatePermissionPayload
): Promise<AccessControlPermission> {
const response = await request<{
data: AccessControlPermission;
message: string;
}>({
url: "/api/access-control/permissions",
method: "post",
data: payload,
});
return response.data;
},
async updatePermission(
payload: UpdatePermissionPayload
): Promise<AccessControlPermission> {
const { id, ...data } = payload;
const response = await request<{
data: AccessControlPermission;
message: string;
}>({
url: `/api/access-control/permissions/${id}`,
method: "put",
data,
});
return response.data;
},
async deletePermission(id: number): Promise<{ message: string }> {
return request<{ message: string }>({
url: `/api/access-control/permissions/${id}`,
method: "delete",
});
},
};
export default AccessControlService;

View File

@ -4,12 +4,24 @@ export interface UserSummary {
id: number; id: number;
name: string; name: string;
email: string; email: string;
roles?: Array<{
id: number;
name: string;
guard_name: string;
}>;
permissions?: Array<{
id: number;
name: string;
guard_name: string;
}>;
} }
export interface CreateUserPayload { export interface CreateUserPayload {
name: string; name: string;
email: string; email: string;
password?: string | null; password?: string | null;
roles?: string[];
permissions?: string[];
} }
export interface UpdateUserPayload { export interface UpdateUserPayload {
@ -17,6 +29,8 @@ export interface UpdateUserPayload {
name: string; name: string;
email: string; email: string;
password?: string | null; password?: string | null;
roles?: string[];
permissions?: string[];
} }
export const UserService = { export const UserService = {

View File

@ -0,0 +1,237 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import AccessControlService from "@/services/accessControl";
import type {
AccessControlPermission,
AccessControlRole,
CreatePermissionPayload,
CreateRolePayload,
UpdatePermissionPayload,
UpdateRolePayload,
} from "@/services/accessControl";
export const useAccessControlStore = defineStore("accessControl", () => {
const roles = ref<AccessControlRole[]>([]);
const permissions = ref<AccessControlPermission[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const allRoles = computed(() => roles.value);
const allPermissions = computed(() => permissions.value);
const isLoading = computed(() => loading.value);
const hasError = computed(() => error.value !== null);
const setLoading = (value: boolean) => {
loading.value = value;
};
const setError = (value: string | null) => {
error.value = value;
};
const fetchAccessControl = async () => {
setLoading(true);
setError(null);
try {
const response = await AccessControlService.getAccessControl();
roles.value = response.roles;
permissions.value = response.permissions;
return response;
} catch (err: any) {
setError(
err.response?.data?.message ||
err.message ||
"Failed to fetch access control data"
);
throw err;
} finally {
setLoading(false);
}
};
const createRole = async (payload: CreateRolePayload) => {
setLoading(true);
setError(null);
try {
const role = await AccessControlService.createRole(payload);
roles.value.push(role);
return role;
} catch (err: any) {
setError(
err.response?.data?.message || err.message || "Failed to create role"
);
throw err;
} finally {
setLoading(false);
}
};
const updateRole = async (payload: UpdateRolePayload) => {
setLoading(true);
setError(null);
try {
const role = await AccessControlService.updateRole(payload);
const index = roles.value.findIndex((item) => item.id === role.id);
if (index !== -1) {
roles.value[index] = role;
}
return role;
} catch (err: any) {
setError(
err.response?.data?.message || err.message || "Failed to update role"
);
throw err;
} finally {
setLoading(false);
}
};
const deleteRole = async (id: number) => {
setLoading(true);
setError(null);
try {
const response = await AccessControlService.deleteRole(id);
roles.value = roles.value.filter((role) => role.id !== id);
return response;
} catch (err: any) {
setError(
err.response?.data?.message || err.message || "Failed to delete role"
);
throw err;
} finally {
setLoading(false);
}
};
const syncRolePermissions = async (id: number, permissionNames: string[]) => {
setLoading(true);
setError(null);
try {
const role = await AccessControlService.syncRolePermissions({
id,
permissions: permissionNames,
});
const index = roles.value.findIndex((item) => item.id === role.id);
if (index !== -1) {
roles.value[index] = role;
}
return role;
} catch (err: any) {
setError(
err.response?.data?.message ||
err.message ||
"Failed to sync role permissions"
);
throw err;
} finally {
setLoading(false);
}
};
const createPermission = async (payload: CreatePermissionPayload) => {
setLoading(true);
setError(null);
try {
const permission = await AccessControlService.createPermission(payload);
permissions.value.push(permission);
return permission;
} catch (err: any) {
setError(
err.response?.data?.message ||
err.message ||
"Failed to create permission"
);
throw err;
} finally {
setLoading(false);
}
};
const updatePermission = async (payload: UpdatePermissionPayload) => {
setLoading(true);
setError(null);
try {
const permission = await AccessControlService.updatePermission(payload);
const index = permissions.value.findIndex(
(item) => item.id === permission.id
);
if (index !== -1) {
permissions.value[index] = permission;
}
return permission;
} catch (err: any) {
setError(
err.response?.data?.message ||
err.message ||
"Failed to update permission"
);
throw err;
} finally {
setLoading(false);
}
};
const deletePermission = async (id: number) => {
setLoading(true);
setError(null);
try {
const response = await AccessControlService.deletePermission(id);
permissions.value = permissions.value.filter(
(permission) => permission.id !== id
);
roles.value = roles.value.map((role) => ({
...role,
permissions: role.permissions?.filter(
(permission) => permission.id !== id
),
}));
return response;
} catch (err: any) {
setError(
err.response?.data?.message ||
err.message ||
"Failed to delete permission"
);
throw err;
} finally {
setLoading(false);
}
};
return {
roles,
permissions,
loading,
error,
allRoles,
allPermissions,
isLoading,
hasError,
fetchAccessControl,
createRole,
updateRole,
deleteRole,
syncRolePermissions,
createPermission,
updatePermission,
deletePermission,
};
});
export default useAccessControlStore;

View File

@ -1,11 +1,730 @@
<template> <template>
<div> <ClientDetailTemplate>
<h1>Gestion des droits</h1> <template #button-return>
</div> <div class="col-12">
<router-link
to="/dashboard-default"
class="btn btn-outline-secondary btn-sm mb-3"
>
<i class="fas fa-arrow-left me-2"></i>Retour au tableau de bord
</router-link>
</div>
</template>
<template #client-detail-sidebar>
<div class="card position-sticky top-1 rights-sidebar-card">
<div class="card-body text-center">
<div class="avatar avatar-xxl position-relative mx-auto mb-3">
<div
class="avatar-placeholder w-100 border-radius-lg shadow-sm d-flex align-items-center justify-content-center bg-gradient-primary"
>
<span class="text-white text-lg font-weight-bold">DR</span>
</div>
</div>
<h5 class="mb-1">Gestion des droits</h5>
<p class="text-sm text-muted mb-3">
Administrer les rôles et permissions applicatives.
</p>
<div class="row gx-2 gy-2 text-start mb-3">
<div class="col-6">
<div class="rights-stat-card">
<span class="rights-stat-label">Rôles</span>
<strong>{{ roleCount }}</strong>
</div>
</div>
<div class="col-6">
<div class="rights-stat-card">
<span class="rights-stat-label">Permissions</span>
<strong>{{ permissionCount }}</strong>
</div>
</div>
</div>
</div>
<hr class="horizontal dark my-0 mx-3" />
<div class="card-body pt-3">
<ul class="nav nav-pills flex-column bg-transparent border-radius-lg">
<li class="nav-item">
<a
class="nav-link rights-nav-link"
:class="{ active: activeTab === 'overview' }"
href="javascript:;"
@click="activeTab = 'overview'"
>
<i class="fas fa-shield-alt me-2"></i>
<span class="text-sm me-2">Vue d'ensemble</span>
</a>
</li>
<li class="nav-item pt-2">
<a
class="nav-link rights-nav-link"
:class="{ active: activeTab === 'roles' }"
href="javascript:;"
@click="activeTab = 'roles'"
>
<i class="fas fa-user-shield me-2"></i>
<span class="text-sm me-2">Rôles</span>
<span class="badge badge-sm bg-gradient-success ms-auto">
{{ roleCount }}
</span>
</a>
</li>
<li class="nav-item pt-2">
<a
class="nav-link rights-nav-link"
:class="{ active: activeTab === 'permissions' }"
href="javascript:;"
@click="activeTab = 'permissions'"
>
<i class="fas fa-key me-2"></i>
<span class="text-sm me-2">Permissions</span>
<span class="badge badge-sm bg-gradient-success ms-auto">
{{ permissionCount }}
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
<template #client-detail-content>
<div
v-if="
store.isLoading && !store.roles.length && !store.permissions.length
"
class="text-center p-5"
>
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div v-else>
<div v-if="store.error" class="alert alert-danger text-white">
{{ store.error }}
</div>
<div v-show="activeTab === 'overview'" class="row g-4">
<div class="col-12 col-xl-6">
<div class="card h-100 rights-card">
<div class="card-header pb-0">
<h5 class="mb-1">Résumé des rôles</h5>
<p class="text-sm text-muted mb-0">
Vue rapide des rôles disponibles et du nombre d'utilisateurs
liés.
</p>
</div>
<div class="card-body pt-3">
<div
v-for="role in store.allRoles"
:key="`overview-role-${role.id}`"
class="rights-list-item"
>
<div>
<h6 class="mb-1 text-sm">{{ role.name }}</h6>
<p class="text-xs text-muted mb-0">
{{ role.permissions?.length || 0 }} permission(s)
</p>
</div>
<span class="badge bg-gradient-dark">
{{ role.users_count || 0 }} utilisateur(s)
</span>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card h-100 rights-card">
<div class="card-header pb-0">
<h5 class="mb-1">Catalogue des permissions</h5>
<p class="text-sm text-muted mb-0">
Permissions publiées et rôles actuellement associés.
</p>
</div>
<div class="card-body pt-3">
<div
v-for="permission in store.allPermissions"
:key="`overview-permission-${permission.id}`"
class="rights-list-item"
>
<div>
<h6 class="mb-1 text-sm">{{ permission.name }}</h6>
<p class="text-xs text-muted mb-0">
{{ formatRelatedRoles(permission.roles) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-show="activeTab === 'roles'" class="row g-4">
<div class="col-12 col-xl-5">
<div class="card rights-card">
<div
class="card-header pb-0 d-flex justify-content-between align-items-start"
>
<div>
<h5 class="mb-1">
{{ roleForm.id ? "Modifier le rôle" : "Créer un rôle" }}
</h5>
<p class="text-sm text-muted mb-0">
Définir un rôle et les permissions qui lui sont attribuées.
</p>
</div>
<button
v-if="roleForm.id"
type="button"
class="btn btn-outline-secondary btn-sm mb-0"
color="secondary"
@click="resetRoleForm"
>
Réinitialiser
</button>
</div>
<div class="card-body pt-3">
<label class="form-label">Nom du rôle</label>
<SoftInput
id="role-name"
v-model="roleForm.name"
placeholder="Ex: manager"
/>
<div class="mt-4">
<label class="form-label d-block mb-3"
>Permissions associées</label
>
<div class="rights-checkbox-grid">
<label
v-for="permission in store.allPermissions"
:key="`role-permission-${permission.id}`"
class="rights-checkbox-item"
>
<input
v-model="roleForm.permissions"
type="checkbox"
:value="permission.name"
/>
<span>{{ permission.name }}</span>
</label>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button
type="button"
class="btn bg-gradient-success mb-0"
:disabled="store.isLoading || !roleForm.name.trim()"
@click="submitRoleForm"
>
{{ roleForm.id ? "Mettre à jour" : "Créer le rôle" }}
</button>
<button
type="button"
class="btn btn-outline-secondary mb-0"
@click="resetRoleForm"
>
Annuler
</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card rights-card">
<div class="card-header pb-0">
<h5 class="mb-1">Liste des rôles</h5>
<p class="text-sm text-muted mb-0">
Modifier, supprimer ou auditer les rôles configurés.
</p>
</div>
<div class="card-body pt-3">
<div v-if="!store.allRoles.length" class="text-sm text-muted">
Aucun rôle disponible pour le moment.
</div>
<div
v-for="role in store.allRoles"
:key="role.id"
class="rights-record-card"
>
<div
class="d-flex justify-content-between align-items-start gap-3 flex-wrap"
>
<div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<h6 class="mb-0">{{ role.name }}</h6>
<span class="badge bg-gradient-dark">
{{ role.users_count || 0 }} utilisateur(s)
</span>
</div>
<p class="text-sm text-muted mt-2 mb-2">
{{ role.permissions?.length || 0 }} permission(s)
liée(s)
</p>
<div class="d-flex flex-wrap gap-2">
<span
v-for="permission in role.permissions || []"
:key="`role-chip-${role.id}-${permission.id}`"
class="badge bg-gradient-light text-dark"
>
{{ permission.name }}
</span>
</div>
</div>
<div class="d-flex gap-2">
<button
type="button"
class="btn btn-outline-info btn-sm mb-0"
@click="startEditRole(role)"
>
Modifier
</button>
<button
type="button"
class="btn btn-outline-danger btn-sm mb-0"
@click="deleteRole(role.id)"
>
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-show="activeTab === 'permissions'" class="row g-4">
<div class="col-12 col-xl-5">
<div class="card rights-card">
<div
class="card-header pb-0 d-flex justify-content-between align-items-start"
>
<div>
<h5 class="mb-1">
{{
permissionForm.id
? "Modifier la permission"
: "Créer une permission"
}}
</h5>
<p class="text-sm text-muted mb-0">
Définir les permissions disponibles dans l'application.
</p>
</div>
<button
v-if="permissionForm.id"
type="button"
class="btn btn-outline-secondary btn-sm mb-0"
color="secondary"
@click="resetPermissionForm"
>
Réinitialiser
</button>
</div>
<div class="card-body pt-3">
<label class="form-label">Nom de la permission</label>
<SoftInput
id="permission-name"
v-model="permissionForm.name"
placeholder="Ex: employee_agenda.view"
/>
<div class="d-flex gap-2 mt-4">
<button
type="button"
class="btn bg-gradient-success mb-0"
:disabled="store.isLoading || !permissionForm.name.trim()"
@click="submitPermissionForm"
>
{{
permissionForm.id
? "Mettre à jour"
: "Créer la permission"
}}
</button>
<button
type="button"
class="btn btn-outline-secondary mb-0"
@click="resetPermissionForm"
>
Annuler
</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card rights-card">
<div class="card-header pb-0">
<h5 class="mb-1">Liste des permissions</h5>
<p class="text-sm text-muted mb-0">
Permissions publiées et rôles qui les consomment.
</p>
</div>
<div class="card-body pt-3">
<div
v-if="!store.allPermissions.length"
class="text-sm text-muted"
>
Aucune permission disponible pour le moment.
</div>
<div
v-for="permission in store.allPermissions"
:key="permission.id"
class="rights-record-card"
>
<div
class="d-flex justify-content-between align-items-start gap-3 flex-wrap"
>
<div>
<h6 class="mb-1">{{ permission.name }}</h6>
<p class="text-sm text-muted mb-2">
{{ formatRelatedRoles(permission.roles) }}
</p>
</div>
<div class="d-flex gap-2">
<button
type="button"
class="btn btn-outline-info btn-sm mb-0"
@click="startEditPermission(permission)"
>
Modifier
</button>
<button
type="button"
class="btn btn-outline-danger btn-sm mb-0"
@click="deletePermission(permission.id)"
>
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</ClientDetailTemplate>
</template> </template>
<script> <script setup>
export default { import { computed, onMounted, reactive, ref } from "vue";
name: "GestionDroits", import Swal from "sweetalert2";
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
import SoftInput from "@/components/SoftInput.vue";
import { useAccessControlStore } from "@/stores/accessControlStore";
const store = useAccessControlStore();
const activeTab = ref("overview");
const roleForm = reactive({
id: null,
name: "",
permissions: [],
});
const permissionForm = reactive({
id: null,
name: "",
});
const roleCount = computed(() => store.allRoles.length);
const permissionCount = computed(() => store.allPermissions.length);
onMounted(async () => {
try {
await store.fetchAccessControl();
} catch {
// Handled by store.error and rendered in-page.
}
});
const resetRoleForm = () => {
roleForm.id = null;
roleForm.name = "";
roleForm.permissions = [];
};
const resetPermissionForm = () => {
permissionForm.id = null;
permissionForm.name = "";
};
const startEditRole = (role) => {
activeTab.value = "roles";
roleForm.id = role.id;
roleForm.name = role.name;
roleForm.permissions = (role.permissions || []).map(
(permission) => permission.name
);
};
const startEditPermission = (permission) => {
activeTab.value = "permissions";
permissionForm.id = permission.id;
permissionForm.name = permission.name;
};
const submitRoleForm = async () => {
try {
if (roleForm.id) {
await store.updateRole({
id: roleForm.id,
name: roleForm.name.trim(),
permissions: roleForm.permissions,
});
} else {
await store.createRole({
name: roleForm.name.trim(),
permissions: roleForm.permissions,
});
}
await store.fetchAccessControl();
resetRoleForm();
await Swal.fire({
icon: "success",
title: "Succès",
text: "Le rôle a été enregistré avec succès.",
confirmButtonText: "Fermer",
});
} catch (error) {
await Swal.fire({
icon: "error",
title: "Erreur",
text: store.error || "Impossible d'enregistrer le rôle.",
confirmButtonText: "Fermer",
});
}
};
const submitPermissionForm = async () => {
try {
if (permissionForm.id) {
await store.updatePermission({
id: permissionForm.id,
name: permissionForm.name.trim(),
});
} else {
await store.createPermission({
name: permissionForm.name.trim(),
});
}
await store.fetchAccessControl();
resetPermissionForm();
await Swal.fire({
icon: "success",
title: "Succès",
text: "La permission a été enregistrée avec succès.",
confirmButtonText: "Fermer",
});
} catch (error) {
await Swal.fire({
icon: "error",
title: "Erreur",
text: store.error || "Impossible d'enregistrer la permission.",
confirmButtonText: "Fermer",
});
}
};
const deleteRole = async (id) => {
const result = await Swal.fire({
title: "Supprimer ce rôle ?",
text: "Cette action retirera le rôle de la configuration courante.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Supprimer",
cancelButtonText: "Annuler",
});
if (!result.isConfirmed) {
return;
}
try {
await store.deleteRole(id);
await store.fetchAccessControl();
if (roleForm.id === id) {
resetRoleForm();
}
} catch (error) {
await Swal.fire({
icon: "error",
title: "Erreur",
text: store.error || "Impossible de supprimer le rôle.",
confirmButtonText: "Fermer",
});
}
};
const deletePermission = async (id) => {
const result = await Swal.fire({
title: "Supprimer cette permission ?",
text: "Cette action retirera la permission de la configuration courante.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Supprimer",
cancelButtonText: "Annuler",
});
if (!result.isConfirmed) {
return;
}
try {
await store.deletePermission(id);
await store.fetchAccessControl();
if (permissionForm.id === id) {
resetPermissionForm();
}
roleForm.permissions = roleForm.permissions.filter((permissionName) =>
store.allPermissions.some(
(permission) => permission.name === permissionName
)
);
} catch (error) {
await Swal.fire({
icon: "error",
title: "Erreur",
text: store.error || "Impossible de supprimer la permission.",
confirmButtonText: "Fermer",
});
}
};
const formatRelatedRoles = (roles = []) => {
if (!roles.length) {
return "Aucun rôle associé";
}
return roles.map((role) => role.name).join(", ");
}; };
</script> </script>
<style scoped>
.position-sticky {
top: 1rem;
}
.rights-sidebar-card,
.rights-card {
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
.avatar-placeholder {
width: 100px;
height: 100px;
font-size: 2rem;
}
.rights-nav-link {
border-radius: 0.5rem;
margin-bottom: 0.25rem;
color: #67748e;
background-color: transparent;
transition: all 0.2s ease-in-out;
}
.rights-nav-link:hover {
background-color: #f8f9fa;
}
.rights-nav-link.active {
background: linear-gradient(310deg, #7928ca, #ff0080);
color: #fff;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.09),
0 2px 3px -1px rgba(0, 0, 0, 0.07);
}
.rights-stat-card {
padding: 0.85rem 1rem;
border-radius: 0.85rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.95),
rgba(244, 247, 250, 0.92)
);
border: 1px solid rgba(103, 116, 142, 0.12);
}
.rights-stat-card strong {
display: block;
font-size: 1.35rem;
color: #344767;
}
.rights-stat-label {
display: block;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #8392ab;
margin-bottom: 0.35rem;
}
.rights-list-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.9rem 0;
border-bottom: 1px solid rgba(103, 116, 142, 0.12);
}
.rights-list-item:last-child,
.rights-record-card:last-child {
border-bottom: 0;
}
.rights-record-card {
padding: 1rem 0;
border-bottom: 1px solid rgba(103, 116, 142, 0.12);
}
.rights-checkbox-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.rights-checkbox-item {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.75rem 0.9rem;
border-radius: 0.85rem;
border: 1px solid rgba(103, 116, 142, 0.15);
background: #fff;
font-size: 0.875rem;
color: #344767;
}
.rights-checkbox-item input {
accent-color: #17c1e8;
}
@media (max-width: 991.98px) {
.rights-checkbox-grid {
grid-template-columns: 1fr;
}
}
</style>