Creation user management interface
This commit is contained in:
parent
3dcb88290d
commit
1d18659bd7
@ -113,8 +113,13 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
$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']);
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ class UpdateUserRequest extends FormRequest
|
||||
'permissions' => ['nullable', 'array'],
|
||||
'permissions.*' => ['string', 'max:150'],
|
||||
'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