Create display droit access
This commit is contained in:
parent
d275c460b6
commit
e27cce45e0
@ -40,6 +40,11 @@ return [
|
||||
'driver' => 'session',
|
||||
'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()->create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
$this->call(AdminAccessSeeder::class);
|
||||
|
||||
$this->call(ProductCategorySeeder::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;
|
||||
name: string;
|
||||
email: string;
|
||||
roles?: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
guard_name: string;
|
||||
}>;
|
||||
permissions?: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
guard_name: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CreateUserPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
password?: string | null;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateUserPayload {
|
||||
@ -17,6 +29,8 @@ export interface UpdateUserPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
password?: string | null;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
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>
|
||||
<div>
|
||||
<h1>Gestion des droits</h1>
|
||||
<ClientDetailTemplate>
|
||||
<template #button-return>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionDroits",
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
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>
|
||||
|
||||
<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