Creation user management interface
This commit is contained in:
parent
3dcb88290d
commit
1d18659bd7
@ -113,8 +113,13 @@ class UserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
$clearPassword = (bool) ($validated['clear_password'] ?? false);
|
||||||
|
|
||||||
if (empty($validated['password'])) {
|
unset($validated['clear_password']);
|
||||||
|
|
||||||
|
if ($clearPassword) {
|
||||||
|
$validated['password'] = null;
|
||||||
|
} elseif (empty($validated['password'])) {
|
||||||
unset($validated['password']);
|
unset($validated['password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ class UpdateUserRequest extends FormRequest
|
|||||||
'permissions' => ['nullable', 'array'],
|
'permissions' => ['nullable', 'array'],
|
||||||
'permissions.*' => ['string', 'max:150'],
|
'permissions.*' => ['string', 'max:150'],
|
||||||
'password' => ['nullable', 'string', Password::min(8)],
|
'password' => ['nullable', 'string', Password::min(8)],
|
||||||
|
'clear_password' => ['nullable', 'boolean'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<ClientDetailTemplate>
|
||||||
|
<template #button-return>
|
||||||
|
<div class="col-12">
|
||||||
|
<router-link
|
||||||
|
to="/parametrage/utilisateurs"
|
||||||
|
class="btn btn-outline-secondary btn-sm mb-3"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour à la liste utilisateurs
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-detail-sidebar>
|
||||||
|
<div class="card position-sticky top-1 user-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-info"
|
||||||
|
>
|
||||||
|
<span class="text-white text-lg font-weight-bold">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-1">Création utilisateur</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Créer un compte puis lui assigner un ou plusieurs rôles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-detail-content>
|
||||||
|
<div v-if="error" class="alert alert-danger text-white">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12 col-xl-8">
|
||||||
|
<UserFormPanel
|
||||||
|
:form="form"
|
||||||
|
:available-roles="roles"
|
||||||
|
:available-permissions="permissions"
|
||||||
|
:disabled="submitDisabled"
|
||||||
|
heading="Créer un utilisateur"
|
||||||
|
description="Renseigner les informations du compte et assigner ses rôles dès la création."
|
||||||
|
submit-label="Créer et assigner"
|
||||||
|
:show-permissions="false"
|
||||||
|
@update:field="$emit('update:field', $event)"
|
||||||
|
@toggle="$emit('toggle-selection', $event)"
|
||||||
|
@submit="$emit('submit-user')"
|
||||||
|
@reset="$emit('reset-form')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-4">
|
||||||
|
<div class="card user-create-card h-100">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Aide rapide</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Conseils pour créer un utilisateur prêt à l'emploi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="help-block">
|
||||||
|
<p class="text-sm mb-2">
|
||||||
|
<strong>1.</strong> Définir nom et email.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mb-2">
|
||||||
|
<strong>2.</strong> Saisir un mot de passe initial.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mb-0">
|
||||||
|
<strong>3.</strong> Assigner le rôle adapté au périmètre
|
||||||
|
métier.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientDetailTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||||
|
import UserFormPanel from "@/components/molecules/parametrage/users/UserFormPanel.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
submitDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:field", "toggle-selection", "submit-user", "reset-form"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.position-sticky {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-sidebar-card,
|
||||||
|
.user-create-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,497 @@
|
|||||||
|
<template>
|
||||||
|
<ClientDetailTemplate>
|
||||||
|
<template #button-return>
|
||||||
|
<div class="col-12">
|
||||||
|
<router-link
|
||||||
|
to="/parametrage/utilisateurs"
|
||||||
|
class="btn btn-outline-secondary btn-sm mb-3"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour à la liste utilisateurs
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-detail-sidebar>
|
||||||
|
<div class="card position-sticky top-1 user-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-dark"
|
||||||
|
>
|
||||||
|
<span class="text-white text-lg font-weight-bold">
|
||||||
|
{{ initials }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-1">{{ user?.name || "Utilisateur" }}</h5>
|
||||||
|
<p class="text-sm text-muted mb-3">{{ user?.email || "-" }}</p>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-2 mb-3">
|
||||||
|
<span
|
||||||
|
v-for="role in user?.roles || []"
|
||||||
|
:key="`sidebar-role-${role.id}`"
|
||||||
|
class="badge bg-gradient-dark"
|
||||||
|
>
|
||||||
|
{{ role.name }}
|
||||||
|
</span>
|
||||||
|
</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 user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'overview' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'overview')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye 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 user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'email' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'email')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-envelope me-2"></i>
|
||||||
|
<span class="text-sm me-2">Modification email</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item pt-2">
|
||||||
|
<a
|
||||||
|
class="nav-link user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'access' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'access')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-shield-alt me-2"></i>
|
||||||
|
<span class="text-sm me-2">Droits et permissions</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item pt-2">
|
||||||
|
<a
|
||||||
|
class="nav-link user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'password' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'password')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key me-2"></i>
|
||||||
|
<span class="text-sm me-2">Reset password</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-detail-content>
|
||||||
|
<div v-if="isLoading" 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="error" class="alert alert-danger text-white">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'overview'" class="row g-4">
|
||||||
|
<div class="col-12 col-xl-7">
|
||||||
|
<div class="card user-detail-card h-100">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Informations générales</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Informations de base et configuration du compte utilisateur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-1">Nom</p>
|
||||||
|
<h6 class="mb-0">{{ user?.name || "-" }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-1">Email</p>
|
||||||
|
<h6 class="mb-0">{{ user?.email || "-" }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Rôles</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="role in user?.roles || []"
|
||||||
|
:key="`overview-role-${role.id}`"
|
||||||
|
class="badge bg-gradient-dark"
|
||||||
|
>
|
||||||
|
{{ role.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!(user?.roles || []).length"
|
||||||
|
class="text-sm text-muted"
|
||||||
|
>
|
||||||
|
Aucun rôle assigné
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">
|
||||||
|
Permissions directes
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="permission in user?.permissions || []"
|
||||||
|
:key="`overview-permission-${permission.id}`"
|
||||||
|
class="badge bg-gradient-info"
|
||||||
|
>
|
||||||
|
{{ permission.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!(user?.permissions || []).length"
|
||||||
|
class="text-sm text-muted"
|
||||||
|
>
|
||||||
|
Aucune permission directe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-5">
|
||||||
|
<div class="card user-detail-card h-100">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Actions rapides</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Accès direct aux opérations principales sur ce compte.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3 d-grid gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info mb-0"
|
||||||
|
@click="$emit('change-tab', 'email')"
|
||||||
|
>
|
||||||
|
Modifier l'email
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-dark mb-0"
|
||||||
|
@click="$emit('change-tab', 'access')"
|
||||||
|
>
|
||||||
|
Gérer les accès
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-warning mb-0"
|
||||||
|
@click="$emit('change-tab', 'password')"
|
||||||
|
>
|
||||||
|
Reset password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'email'" class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card user-detail-card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Modification email</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Mettre à jour l'adresse email du compte utilisateur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<label class="form-label">Nouvelle adresse email</label>
|
||||||
|
<SoftInput
|
||||||
|
id="user-detail-email"
|
||||||
|
:model-value="emailForm"
|
||||||
|
type="email"
|
||||||
|
placeholder="Ex: utilisateur@example.com"
|
||||||
|
@update:model-value="$emit('update:email', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mt-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn bg-gradient-info mb-0"
|
||||||
|
:disabled="submitDisabled"
|
||||||
|
@click="$emit('submit-email')"
|
||||||
|
>
|
||||||
|
Enregistrer l'email
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
@click="$emit('reset-email')"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'access'" class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card user-detail-card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Rôles et permissions</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Modifier les rôles assignés et les permissions directes de
|
||||||
|
l'utilisateur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label d-block mb-3">Rôles assignés</label>
|
||||||
|
<div class="access-checkbox-grid">
|
||||||
|
<label
|
||||||
|
v-for="role in availableRoles"
|
||||||
|
:key="`detail-role-${role.id}`"
|
||||||
|
class="access-checkbox-item"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="selectedRoles.includes(role.name)"
|
||||||
|
type="checkbox"
|
||||||
|
@change="
|
||||||
|
$emit('toggle-access', {
|
||||||
|
field: 'roles',
|
||||||
|
value: role.name,
|
||||||
|
checked: $event.target.checked,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span>{{ role.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label d-block mb-3">
|
||||||
|
Permissions directes
|
||||||
|
</label>
|
||||||
|
<div class="access-checkbox-grid">
|
||||||
|
<label
|
||||||
|
v-for="permission in availablePermissions"
|
||||||
|
:key="`detail-permission-${permission.id}`"
|
||||||
|
class="access-checkbox-item"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="selectedPermissions.includes(permission.name)"
|
||||||
|
type="checkbox"
|
||||||
|
@change="
|
||||||
|
$emit('toggle-access', {
|
||||||
|
field: 'permissions',
|
||||||
|
value: permission.name,
|
||||||
|
checked: $event.target.checked,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span>{{ permission.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mt-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn bg-gradient-dark mb-0"
|
||||||
|
:disabled="accessSubmitDisabled"
|
||||||
|
@click="$emit('submit-access')"
|
||||||
|
>
|
||||||
|
Enregistrer les accès
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
@click="$emit('reset-access')"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'password'" class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card user-detail-card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Reset password</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Supprimer le mot de passe actuel de ce compte utilisateur.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="alert alert-warning text-white mb-0">
|
||||||
|
Cette action va supprimer le mot de passe stocké en base pour
|
||||||
|
cet utilisateur. Une confirmation sera demandée avant
|
||||||
|
suppression.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mt-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn bg-gradient-warning mb-0"
|
||||||
|
:disabled="submitDisabled"
|
||||||
|
@click="$emit('submit-password')"
|
||||||
|
>
|
||||||
|
Supprimer le mot de passe
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
@click="$emit('reset-password')"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientDetailTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineEmits, defineProps } from "vue";
|
||||||
|
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
emailForm: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
availableRoles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
availablePermissions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
selectedRoles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
selectedPermissions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
submitDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
accessSubmitDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits([
|
||||||
|
"change-tab",
|
||||||
|
"update:email",
|
||||||
|
"submit-email",
|
||||||
|
"submit-password",
|
||||||
|
"reset-email",
|
||||||
|
"reset-password",
|
||||||
|
"toggle-access",
|
||||||
|
"submit-access",
|
||||||
|
"reset-access",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const initials = computed(() => {
|
||||||
|
return (props.user?.name || "US")
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.position-sticky {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-sidebar-card,
|
||||||
|
.user-detail-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #67748e;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link.active {
|
||||||
|
background: linear-gradient(310deg, #1f2937, #4b5563);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-checkbox-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-checkbox-item input {
|
||||||
|
accent-color: #344767;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,336 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card user-list-hero border-0 overflow-hidden">
|
||||||
|
<div class="card-body p-4 p-lg-5">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<p class="text-sm text-uppercase fw-bold opacity-8 mb-2">
|
||||||
|
Paramétrage
|
||||||
|
</p>
|
||||||
|
<h3 class="text-white mb-2">Gestion des utilisateurs</h3>
|
||||||
|
<p class="text-white-50 mb-0">
|
||||||
|
Liste paginée des comptes applicatifs avec accès rapide au
|
||||||
|
détail, à la modification d'email et au reset de mot de passe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-4 mt-4 mt-lg-0 text-lg-end">
|
||||||
|
<router-link
|
||||||
|
to="/parametrage/utilisateurs/creation"
|
||||||
|
class="btn bg-gradient-light mb-0"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Créer un utilisateur
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card border-0 h-100 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Utilisateurs</p>
|
||||||
|
<h4 class="mb-0">{{ totalUsers }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card border-0 h-100 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Rôles</p>
|
||||||
|
<h4 class="mb-0">{{ roleCount }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card border-0 h-100 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Page actuelle</p>
|
||||||
|
<h4 class="mb-0">{{ pagination.current_page }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div
|
||||||
|
class="card-header pb-0 d-flex flex-wrap gap-3 align-items-center justify-content-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Liste des utilisateurs</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||||
|
{{ totalUsers }} utilisateurs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="user-search-box">
|
||||||
|
<SoftInput
|
||||||
|
id="user-list-search"
|
||||||
|
:model-value="search"
|
||||||
|
placeholder="Rechercher par nom ou email"
|
||||||
|
icon="fas fa-search"
|
||||||
|
icon-dir="left"
|
||||||
|
@update:model-value="$emit('update:search', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body px-0 pt-0 pb-2">
|
||||||
|
<div v-if="error" class="alert alert-danger text-white mx-3 mt-3 mb-0">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-responsive p-0">
|
||||||
|
<table class="table align-items-center mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-4"
|
||||||
|
>
|
||||||
|
Utilisateur
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Rôles
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-center"
|
||||||
|
>
|
||||||
|
Permissions
|
||||||
|
</th>
|
||||||
|
<th class="text-secondary opacity-7"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in users" :key="user.id">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex px-4 py-3 align-items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="avatar avatar-sm border-radius-lg bg-gradient-dark d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<span class="text-white text-xs font-weight-bold">
|
||||||
|
{{ getInitials(user.name) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0 text-sm">{{ user.name }}</h6>
|
||||||
|
<p class="text-xs text-secondary mb-0">#{{ user.id }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="text-sm font-weight-bold mb-0">{{ user.email }}</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-wrap gap-2 py-2">
|
||||||
|
<span
|
||||||
|
v-for="role in user.roles || []"
|
||||||
|
:key="`role-${user.id}-${role.id}`"
|
||||||
|
class="badge bg-gradient-dark"
|
||||||
|
>
|
||||||
|
{{ role.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!(user.roles || []).length"
|
||||||
|
class="text-xs text-secondary"
|
||||||
|
>
|
||||||
|
Aucun rôle
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-center text-sm">
|
||||||
|
<span class="badge bg-gradient-info">
|
||||||
|
{{ (user.permissions || []).length }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle pe-4 text-end">
|
||||||
|
<router-link
|
||||||
|
:to="`/parametrage/utilisateurs/${user.id}`"
|
||||||
|
class="text-secondary font-weight-bold text-xs me-3"
|
||||||
|
>
|
||||||
|
Détail
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
href="javascript:;"
|
||||||
|
class="text-danger font-weight-bold text-xs"
|
||||||
|
@click="$emit('delete-user', user.id)"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="!users.length">
|
||||||
|
<td colspan="5" class="text-center py-4">
|
||||||
|
<p class="text-sm mb-0">Aucun utilisateur trouvé.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isLoading && pagination.last_page > 1"
|
||||||
|
class="d-flex justify-content-between align-items-center mt-3 px-4 flex-wrap gap-3"
|
||||||
|
>
|
||||||
|
<div class="text-xs text-secondary font-weight-bold">
|
||||||
|
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||||
|
{{ totalUsers }} utilisateurs
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination utilisateurs">
|
||||||
|
<ul class="pagination pagination-sm pagination-info mb-0">
|
||||||
|
<li
|
||||||
|
class="page-item"
|
||||||
|
:class="{ disabled: pagination.current_page === 1 }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="
|
||||||
|
$emit('change-page', pagination.current_page - 1)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
v-for="page in displayedPages"
|
||||||
|
:key="`page-${page}`"
|
||||||
|
class="page-item"
|
||||||
|
:class="{
|
||||||
|
active: pagination.current_page === page,
|
||||||
|
disabled: page === '...',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="page !== '...' && $emit('change-page', page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
class="page-item"
|
||||||
|
:class="{
|
||||||
|
disabled: pagination.current_page === pagination.last_page,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="
|
||||||
|
$emit('change-page', pagination.current_page + 1)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineEmits, defineProps } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
totalUsers: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
roleCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:search", "change-page", "delete-user"]);
|
||||||
|
|
||||||
|
const displayedPages = computed(() => {
|
||||||
|
const total = Number(props.pagination?.last_page) || 1;
|
||||||
|
const current = Number(props.pagination?.current_page) || 1;
|
||||||
|
|
||||||
|
if (total <= 5) {
|
||||||
|
return Array.from({ length: total }, (_, index) => index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current <= 3) {
|
||||||
|
return [1, 2, 3, 4, "...", total];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current >= total - 2) {
|
||||||
|
return [1, "...", total - 3, total - 2, total - 1, total];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [1, "...", current - 1, current, current + 1, "...", total];
|
||||||
|
});
|
||||||
|
|
||||||
|
const getInitials = (name = "") => {
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-list-hero {
|
||||||
|
background: linear-gradient(135deg, #111827 0%, #374151 50%, #0ea5e9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-box {
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.user-search-box {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<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 user-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-dark"
|
||||||
|
>
|
||||||
|
<span class="text-white text-lg font-weight-bold">US</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-1">Gestion utilisateurs</h5>
|
||||||
|
<p class="text-sm text-muted mb-3">
|
||||||
|
Piloter les comptes applicatifs, rôles et permissions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row gx-2 gy-2 text-start mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="user-stat-card">
|
||||||
|
<span class="user-stat-label">Utilisateurs</span>
|
||||||
|
<strong>{{ userCount }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="user-stat-card">
|
||||||
|
<span class="user-stat-label">Rôles</span>
|
||||||
|
<strong>{{ roleCount }}</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 user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'overview' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'overview')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-users 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 user-nav-link"
|
||||||
|
:class="{ active: activeTab === 'management' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="$emit('change-tab', 'management')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-cog me-2"></i>
|
||||||
|
<span class="text-sm me-2">Utilisateurs</span>
|
||||||
|
<span class="badge badge-sm bg-gradient-success ms-auto">
|
||||||
|
{{ userCount }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-detail-content>
|
||||||
|
<div v-if="isLoading" 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="error" class="alert alert-danger text-white">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'overview'" class="row g-4">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="card user-management-card h-100">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Comptes récents</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Vue rapide des utilisateurs configurés dans la plateforme.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div
|
||||||
|
v-for="user in users.slice(0, 5)"
|
||||||
|
:key="`overview-user-${user.id}`"
|
||||||
|
class="overview-list-item"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 text-sm">{{ user.name }}</h6>
|
||||||
|
<p class="text-xs text-muted mb-0">{{ user.email }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-gradient-dark">
|
||||||
|
{{ (user.roles || []).length }} rôle(s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<div class="card user-management-card h-100">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5 class="mb-1">Permissions disponibles</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Permissions assignables directement ou via rôle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div
|
||||||
|
v-for="permission in permissions.slice(0, 6)"
|
||||||
|
:key="`overview-permission-${permission.id}`"
|
||||||
|
class="overview-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 === 'management'" class="row g-4">
|
||||||
|
<div class="col-12 col-xl-5">
|
||||||
|
<UserFormPanel
|
||||||
|
:form="form"
|
||||||
|
:available-roles="roles"
|
||||||
|
:available-permissions="permissions"
|
||||||
|
:disabled="submitDisabled"
|
||||||
|
@update:field="$emit('update:field', $event)"
|
||||||
|
@toggle="$emit('toggle-selection', $event)"
|
||||||
|
@submit="$emit('submit-user')"
|
||||||
|
@reset="$emit('reset-form')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-7">
|
||||||
|
<UserListPanel
|
||||||
|
:users="users"
|
||||||
|
:search="search"
|
||||||
|
@edit="$emit('edit-user', $event)"
|
||||||
|
@delete="$emit('delete-user', $event)"
|
||||||
|
@update:search="$emit('update:search', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientDetailTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||||
|
import UserFormPanel from "@/components/molecules/parametrage/users/UserFormPanel.vue";
|
||||||
|
import UserListPanel from "@/components/molecules/parametrage/users/UserListPanel.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
submitDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
userCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
roleCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits([
|
||||||
|
"change-tab",
|
||||||
|
"update:field",
|
||||||
|
"toggle-selection",
|
||||||
|
"submit-user",
|
||||||
|
"reset-form",
|
||||||
|
"edit-user",
|
||||||
|
"delete-user",
|
||||||
|
"update:search",
|
||||||
|
]);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-sidebar-card,
|
||||||
|
.user-management-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #67748e;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nav-link.active {
|
||||||
|
background: linear-gradient(310deg, #1f2937, #4b5563);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.09),
|
||||||
|
0 2px 3px -1px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stat-card strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #8392ab;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-list-item:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card user-management-card h-100">
|
||||||
|
<div
|
||||||
|
class="card-header pb-0 d-flex justify-content-between align-items-start"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">
|
||||||
|
{{
|
||||||
|
heading ||
|
||||||
|
(form.id ? "Modifier l'utilisateur" : "Créer un utilisateur")
|
||||||
|
}}
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
{{
|
||||||
|
description ||
|
||||||
|
"Définir le compte, ses rôles et ses permissions directes."
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="form.id"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm mb-0"
|
||||||
|
@click="$emit('reset')"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label">Nom</label>
|
||||||
|
<SoftInput
|
||||||
|
id="user-name"
|
||||||
|
:model-value="form.name"
|
||||||
|
placeholder="Ex: Admin User"
|
||||||
|
@update:model-value="updateField('name', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mt-3 mt-md-0">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<SoftInput
|
||||||
|
id="user-email"
|
||||||
|
:model-value="form.email"
|
||||||
|
placeholder="Ex: admin@example.com"
|
||||||
|
type="email"
|
||||||
|
@update:model-value="updateField('email', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<label class="form-label">
|
||||||
|
Mot de passe {{ form.id ? "(laisser vide pour conserver)" : "" }}
|
||||||
|
</label>
|
||||||
|
<SoftInput
|
||||||
|
id="user-password"
|
||||||
|
:model-value="form.password"
|
||||||
|
placeholder="Ex: password123"
|
||||||
|
type="password"
|
||||||
|
@update:model-value="updateField('password', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="form-label d-block mb-3">Rôles</label>
|
||||||
|
<div class="user-checkbox-grid">
|
||||||
|
<label
|
||||||
|
v-for="role in availableRoles"
|
||||||
|
:key="`user-role-${role.id}`"
|
||||||
|
class="user-checkbox-item"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="form.roles.includes(role.name)"
|
||||||
|
type="checkbox"
|
||||||
|
@change="toggleSelection('roles', role.name, $event)"
|
||||||
|
/>
|
||||||
|
<span>{{ role.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showPermissions" class="mt-4">
|
||||||
|
<label class="form-label d-block mb-3">Permissions directes</label>
|
||||||
|
<div class="user-checkbox-grid">
|
||||||
|
<label
|
||||||
|
v-for="permission in availablePermissions"
|
||||||
|
:key="`user-permission-${permission.id}`"
|
||||||
|
class="user-checkbox-item"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="form.permissions.includes(permission.name)"
|
||||||
|
type="checkbox"
|
||||||
|
@change="toggleSelection('permissions', permission.name, $event)"
|
||||||
|
/>
|
||||||
|
<span>{{ permission.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mt-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn bg-gradient-success mb-0"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="$emit('submit')"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
submitLabel || (form.id ? "Mettre à jour" : "Créer l'utilisateur")
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
@click="$emit('reset')"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
availableRoles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
availablePermissions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
submitLabel: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
showPermissions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:field", "toggle", "submit", "reset"]);
|
||||||
|
|
||||||
|
const updateField = (field, value) => {
|
||||||
|
emit("update:field", { field, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelection = (field, value, event) => {
|
||||||
|
emit("toggle", { field, value, checked: event.target.checked });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-management-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-checkbox-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-checkbox-item input {
|
||||||
|
accent-color: #17c1e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.user-checkbox-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card user-management-card h-100">
|
||||||
|
<div
|
||||||
|
class="card-header pb-0 d-flex justify-content-between align-items-start gap-3 flex-wrap"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Liste des utilisateurs</h5>
|
||||||
|
<p class="text-sm text-muted mb-0">
|
||||||
|
Gérer les comptes, leurs rôles et leurs permissions associées.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="user-search-box">
|
||||||
|
<SoftInput
|
||||||
|
id="user-search"
|
||||||
|
:model-value="search"
|
||||||
|
placeholder="Rechercher par nom ou email"
|
||||||
|
icon="fas fa-search"
|
||||||
|
icon-dir="left"
|
||||||
|
@update:model-value="$emit('update:search', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<div v-if="!users.length" class="text-sm text-muted">
|
||||||
|
Aucun utilisateur disponible pour le moment.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="user in users" :key="user.id" class="user-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">{{ user.name }}</h6>
|
||||||
|
<span class="badge bg-gradient-light text-dark">
|
||||||
|
{{ user.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Rôles</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="role in user.roles || []"
|
||||||
|
:key="`user-role-chip-${user.id}-${role.id}`"
|
||||||
|
class="badge bg-gradient-dark"
|
||||||
|
>
|
||||||
|
{{ role.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!(user.roles || []).length"
|
||||||
|
class="text-sm text-muted"
|
||||||
|
>
|
||||||
|
Aucun rôle
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-xs text-uppercase text-muted mb-2">Permissions</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="permission in user.permissions || []"
|
||||||
|
:key="`user-permission-chip-${user.id}-${permission.id}`"
|
||||||
|
class="badge bg-gradient-info"
|
||||||
|
>
|
||||||
|
{{ permission.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!(user.permissions || []).length"
|
||||||
|
class="text-sm text-muted"
|
||||||
|
>
|
||||||
|
Aucune permission directe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info btn-sm mb-0"
|
||||||
|
@click="$emit('edit', user)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger btn-sm mb-0"
|
||||||
|
@click="$emit('delete', user.id)"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["edit", "delete", "update:search"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-management-card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-record-card {
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-bottom: 1px solid rgba(103, 116, 142, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-record-card:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-box {
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.user-search-box {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
thanasoft-front/src/views/pages/Parametrage/UserCreate.vue
Normal file
105
thanasoft-front/src/views/pages/Parametrage/UserCreate.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<UserCreatePresentation
|
||||||
|
:form="form"
|
||||||
|
:roles="accessControlStore.allRoles"
|
||||||
|
:permissions="accessControlStore.allPermissions"
|
||||||
|
:error="pageError"
|
||||||
|
:submit-disabled="submitDisabled"
|
||||||
|
@update:field="updateField"
|
||||||
|
@toggle-selection="toggleSelection"
|
||||||
|
@submit-user="submitUser"
|
||||||
|
@reset-form="resetForm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import UserCreatePresentation from "@/components/Organism/Parametrage/Users/UserCreatePresentation.vue";
|
||||||
|
import { useAccessControlStore } from "@/stores/accessControlStore";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const accessControlStore = useAccessControlStore();
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
roles: [],
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageError = computed(() => userStore.error || accessControlStore.error);
|
||||||
|
const submitDisabled = computed(
|
||||||
|
() =>
|
||||||
|
userStore.isLoading ||
|
||||||
|
accessControlStore.isLoading ||
|
||||||
|
!form.name.trim() ||
|
||||||
|
!form.email.trim() ||
|
||||||
|
!form.password.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await accessControlStore.fetchAccessControl();
|
||||||
|
} catch {
|
||||||
|
// Errors are surfaced via stores.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.id = null;
|
||||||
|
form.name = "";
|
||||||
|
form.email = "";
|
||||||
|
form.password = "";
|
||||||
|
form.roles = [];
|
||||||
|
form.permissions = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateField = ({ field, value }) => {
|
||||||
|
form[field] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelection = ({ field, value, checked }) => {
|
||||||
|
if (checked) {
|
||||||
|
if (!form[field].includes(value)) {
|
||||||
|
form[field] = [...form[field], value];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[field] = form[field].filter((item) => item !== value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUser = async () => {
|
||||||
|
try {
|
||||||
|
const user = await userStore.createUser({
|
||||||
|
name: form.name.trim(),
|
||||||
|
email: form.email.trim(),
|
||||||
|
password: form.password.trim(),
|
||||||
|
roles: form.roles,
|
||||||
|
permissions: form.permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Succès",
|
||||||
|
text: "L'utilisateur a été créé avec succès.",
|
||||||
|
confirmButtonText: "Voir le détail",
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/parametrage/utilisateurs/${user.id}`);
|
||||||
|
} catch {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erreur",
|
||||||
|
text: userStore.error || "Impossible de créer l'utilisateur.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
236
thanasoft-front/src/views/pages/Parametrage/UserDetails.vue
Normal file
236
thanasoft-front/src/views/pages/Parametrage/UserDetails.vue
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<UserDetailPresentation
|
||||||
|
:user="userStore.selectedUser"
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:email-form="emailForm"
|
||||||
|
:available-roles="accessControlStore.allRoles"
|
||||||
|
:available-permissions="accessControlStore.allPermissions"
|
||||||
|
:selected-roles="accessRoles"
|
||||||
|
:selected-permissions="accessPermissions"
|
||||||
|
:is-loading="userStore.isLoading"
|
||||||
|
:error="userStore.error"
|
||||||
|
:submit-disabled="submitDisabled"
|
||||||
|
:access-submit-disabled="accessSubmitDisabled"
|
||||||
|
@change-tab="activeTab = $event"
|
||||||
|
@update:email="emailForm = $event"
|
||||||
|
@submit-email="submitEmail"
|
||||||
|
@submit-password="submitPassword"
|
||||||
|
@reset-email="resetEmail"
|
||||||
|
@reset-password="resetPassword"
|
||||||
|
@toggle-access="toggleAccess"
|
||||||
|
@submit-access="submitAccess"
|
||||||
|
@reset-access="resetAccess"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import UserDetailPresentation from "@/components/Organism/Parametrage/Users/UserDetailPresentation.vue";
|
||||||
|
import { useAccessControlStore } from "@/stores/accessControlStore";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const accessControlStore = useAccessControlStore();
|
||||||
|
|
||||||
|
const userId = Number(route.params.id);
|
||||||
|
const activeTab = ref("overview");
|
||||||
|
const emailForm = ref("");
|
||||||
|
const accessRoles = ref([]);
|
||||||
|
const accessPermissions = ref([]);
|
||||||
|
|
||||||
|
const submitDisabled = computed(() => {
|
||||||
|
if (activeTab.value === "email") {
|
||||||
|
return userStore.isLoading || !emailForm.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === "password") {
|
||||||
|
return userStore.isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userStore.isLoading;
|
||||||
|
});
|
||||||
|
|
||||||
|
const accessSubmitDisabled = computed(() => userStore.isLoading);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const [user] = await Promise.all([
|
||||||
|
userStore.fetchUser(userId),
|
||||||
|
accessControlStore.fetchAccessControl(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
emailForm.value = user.email;
|
||||||
|
accessRoles.value = (user.roles || []).map((role) => role.name);
|
||||||
|
accessPermissions.value = (user.permissions || []).map(
|
||||||
|
(permission) => permission.name
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Error already exposed by store.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetEmail = () => {
|
||||||
|
emailForm.value = userStore.selectedUser?.email || "";
|
||||||
|
activeTab.value = "overview";
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPassword = () => {
|
||||||
|
activeTab.value = "overview";
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAccess = () => {
|
||||||
|
accessRoles.value = (userStore.selectedUser?.roles || []).map(
|
||||||
|
(role) => role.name
|
||||||
|
);
|
||||||
|
accessPermissions.value = (userStore.selectedUser?.permissions || []).map(
|
||||||
|
(permission) => permission.name
|
||||||
|
);
|
||||||
|
activeTab.value = "overview";
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyAccessState = (user) => {
|
||||||
|
accessRoles.value = (user?.roles || []).map((role) => role.name);
|
||||||
|
accessPermissions.value = (user?.permissions || []).map(
|
||||||
|
(permission) => permission.name
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAccess = ({ field, value, checked }) => {
|
||||||
|
const target = field === "roles" ? accessRoles : accessPermissions;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
if (!target.value.includes(value)) {
|
||||||
|
target.value = [...target.value, value];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.value = target.value.filter((item) => item !== value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitEmail = async () => {
|
||||||
|
const user = userStore.selectedUser;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userStore.updateUser({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: emailForm.value.trim(),
|
||||||
|
roles: (user.roles || []).map((role) => role.name),
|
||||||
|
permissions: (user.permissions || []).map(
|
||||||
|
(permission) => permission.name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Succès",
|
||||||
|
text: "L'email a été mis à jour avec succès.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
|
||||||
|
activeTab.value = "overview";
|
||||||
|
} catch {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erreur",
|
||||||
|
text: userStore.error || "Impossible de mettre à jour l'email.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPassword = async () => {
|
||||||
|
const user = userStore.selectedUser;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: "Supprimer le mot de passe ?",
|
||||||
|
text:
|
||||||
|
"Cette action va supprimer le mot de passe en base pour cet utilisateur.",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Oui, supprimer",
|
||||||
|
cancelButtonText: "Annuler",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userStore.updateUser({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
clear_password: true,
|
||||||
|
roles: (user.roles || []).map((role) => role.name),
|
||||||
|
permissions: (user.permissions || []).map(
|
||||||
|
(permission) => permission.name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
resetPassword();
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Succès",
|
||||||
|
text: "Le mot de passe a été réinitialisé avec succès.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erreur",
|
||||||
|
text: userStore.error || "Impossible de réinitialiser le mot de passe.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitAccess = async () => {
|
||||||
|
const user = userStore.selectedUser;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedUser = await userStore.updateUser({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
roles: accessRoles.value,
|
||||||
|
permissions: accessPermissions.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyAccessState(updatedUser);
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Succès",
|
||||||
|
text: "Les rôles et permissions ont été mis à jour avec succès.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
|
||||||
|
activeTab.value = "overview";
|
||||||
|
} catch {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erreur",
|
||||||
|
text: userStore.error || "Impossible de mettre à jour les accès.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
131
thanasoft-front/src/views/pages/Parametrage/Users.vue
Normal file
131
thanasoft-front/src/views/pages/Parametrage/Users.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<UserListPresentation
|
||||||
|
:users="paginatedUsers"
|
||||||
|
:search="search"
|
||||||
|
:is-loading="userStore.isLoading"
|
||||||
|
:error="userStore.error"
|
||||||
|
:total-users="filteredUsers.length"
|
||||||
|
:role-count="accessControlStore.allRoles.length"
|
||||||
|
:pagination="pagination"
|
||||||
|
@update:search="updateSearch"
|
||||||
|
@change-page="changePage"
|
||||||
|
@delete-user="deleteUser"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import UserListPresentation from "@/components/Organism/Parametrage/Users/UserListPresentation.vue";
|
||||||
|
import { useAccessControlStore } from "@/stores/accessControlStore";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const accessControlStore = useAccessControlStore();
|
||||||
|
|
||||||
|
const search = ref("");
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const perPage = 10;
|
||||||
|
|
||||||
|
const filteredUsers = computed(() => {
|
||||||
|
const query = search.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return userStore.allUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userStore.allUsers.filter((user) => {
|
||||||
|
return (
|
||||||
|
user.name.toLowerCase().includes(query) ||
|
||||||
|
user.email.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginatedUsers = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * perPage;
|
||||||
|
return filteredUsers.value.slice(start, start + perPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
const total = filteredUsers.value.length;
|
||||||
|
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
||||||
|
const safeCurrentPage = Math.min(currentPage.value, lastPage);
|
||||||
|
const from = total === 0 ? 0 : (safeCurrentPage - 1) * perPage + 1;
|
||||||
|
const to = Math.min(safeCurrentPage * perPage, total);
|
||||||
|
|
||||||
|
return {
|
||||||
|
current_page: safeCurrentPage,
|
||||||
|
last_page: lastPage,
|
||||||
|
per_page: perPage,
|
||||||
|
total,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
userStore.fetchUsers(),
|
||||||
|
accessControlStore.fetchAccessControl(),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
// Errors are surfaced via stores.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSearch = (value) => {
|
||||||
|
search.value = value;
|
||||||
|
currentPage.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePage = (page) => {
|
||||||
|
if (
|
||||||
|
page < 1 ||
|
||||||
|
page > pagination.value.last_page ||
|
||||||
|
page === currentPage.value
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (id) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: "Supprimer cet utilisateur ?",
|
||||||
|
text: "Cette action retirera définitivement le compte utilisateur.",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Supprimer",
|
||||||
|
cancelButtonText: "Annuler",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userStore.deleteUser(id);
|
||||||
|
await userStore.fetchUsers();
|
||||||
|
if (currentPage.value > pagination.value.last_page) {
|
||||||
|
currentPage.value = pagination.value.last_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Succès",
|
||||||
|
text: "L'utilisateur a été supprimé avec succès.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Erreur",
|
||||||
|
text: userStore.error || "Impossible de supprimer l'utilisateur.",
|
||||||
|
confirmButtonText: "Fermer",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user