Create display droit access
This commit is contained in:
parent
d275c460b6
commit
e27cce45e0
@ -40,6 +40,11 @@ return [
|
|||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'sanctum' => [
|
||||||
|
'driver' => 'sanctum',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
39
thanasoft-back/database/seeders/AdminAccessSeeder.php
Normal file
39
thanasoft-back/database/seeders/AdminAccessSeeder.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
151
thanasoft-front/src/services/accessControl.ts
Normal file
151
thanasoft-front/src/services/accessControl.ts
Normal 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;
|
||||||
@ -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 = {
|
||||||
|
|||||||
237
thanasoft-front/src/stores/accessControlStore.ts
Normal file
237
thanasoft-front/src/stores/accessControlStore.ts
Normal 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;
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user