Files
btpxpress-frontend/app/(main)/admin/utilisateurs/page.tsx

992 lines
46 KiB
TypeScript
Executable File

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../../../../contexts/AuthContext';
import ProtectedRoute from '../../../../components/auth/ProtectedRoute';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { InputText } from 'primereact/inputtext';
import { Dialog } from 'primereact/dialog';
import { Dropdown } from 'primereact/dropdown';
import { Tag } from 'primereact/tag';
import { ToggleButton } from 'primereact/togglebutton';
import { Password } from 'primereact/password';
import { Checkbox } from 'primereact/checkbox';
import { Calendar } from 'primereact/calendar';
import { Avatar } from 'primereact/avatar';
import { Badge } from 'primereact/badge';
import { Chip } from 'primereact/chip';
import { Chart } from 'primereact/chart';
import { TabView, TabPanel } from 'primereact/tabview';
import {
ActionButtonGroup,
ViewButton,
EditButton,
DeleteButton,
ActionButton
} from '../../../../components/ui/ActionButton';
interface Utilisateur {
id: string;
nom: string;
prenom: string;
email: string;
telephone: string;
role: 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER';
departement: string;
statut: 'ACTIF' | 'INACTIF' | 'SUSPENDU' | 'CONGE';
actif: boolean;
derniereConnexion: Date;
dateCreation: Date;
dateModification: Date;
permissions: string[];
avatar?: string;
adresse: string;
dateNaissance?: Date;
numeroEmploye: string;
manager?: string;
tentativesConnexion: number;
derniereMiseAJourMotDePasse: Date;
motDePasseExpire: boolean;
deuxFacteursActive: boolean;
sessionActive: boolean;
heuresConnexion: number;
dernierAction: string;
preferences: {
theme: 'light' | 'dark';
langue: string;
notifications: boolean;
timezone: string;
};
}
interface ActiviteUtilisateur {
id: string;
utilisateurId: string;
action: string;
module: string;
timestamp: Date;
details: string;
adresseIP: string;
navigateur: string;
}
const UtilisateursPage = () => {
const [utilisateurs, setUtilisateurs] = useState<Utilisateur[]>([]);
const [activites, setActivites] = useState<ActiviteUtilisateur[]>([]);
const [selectedUtilisateurs, setSelectedUtilisateurs] = useState<Utilisateur[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [utilisateurDialog, setUtilisateurDialog] = useState(false);
const [activiteDialog, setActiviteDialog] = useState(false);
const [deleteDialog, setDeleteDialog] = useState(false);
const [resetPasswordDialog, setResetPasswordDialog] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const [utilisateur, setUtilisateur] = useState<Utilisateur>({
id: '',
nom: '',
prenom: '',
email: '',
telephone: '',
role: 'USER',
departement: '',
statut: 'ACTIF',
actif: true,
derniereConnexion: new Date(),
dateCreation: new Date(),
dateModification: new Date(),
permissions: [],
adresse: '',
numeroEmploye: '',
tentativesConnexion: 0,
derniereMiseAJourMotDePasse: new Date(),
motDePasseExpire: false,
deuxFacteursActive: false,
sessionActive: false,
heuresConnexion: 0,
dernierAction: '',
preferences: {
theme: 'light',
langue: 'fr',
notifications: true,
timezone: 'Europe/Paris'
}
});
const [submitted, setSubmitted] = useState(false);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Utilisateur[]>>(null);
const roles = [
{ label: 'Administrateur', value: 'ADMIN' },
{ label: 'Manager', value: 'MANAGER' },
{ label: 'Utilisateur', value: 'USER' },
{ label: 'Lecture seule', value: 'VIEWER' }
];
const departements = [
{ label: 'Direction', value: 'direction' },
{ label: 'Commercial', value: 'commercial' },
{ label: 'Technique', value: 'technique' },
{ label: 'Administratif', value: 'administratif' },
{ label: 'Chantier', value: 'chantier' },
{ label: 'Maintenance', value: 'maintenance' }
];
const statuts = [
{ label: 'Actif', value: 'ACTIF' },
{ label: 'Inactif', value: 'INACTIF' },
{ label: 'Suspendu', value: 'SUSPENDU' },
{ label: 'Congé', value: 'CONGE' }
];
const availablePermissions = [
'dashboard.read',
'clients.read', 'clients.write', 'clients.delete',
'chantiers.read', 'chantiers.write', 'chantiers.delete',
'devis.read', 'devis.write', 'devis.delete',
'factures.read', 'factures.write', 'factures.delete',
'stock.read', 'stock.write', 'stock.delete',
'planning.read', 'planning.write',
'rapports.read', 'rapports.write',
'administration.read', 'administration.write',
'utilisateurs.read', 'utilisateurs.write', 'utilisateurs.delete',
'backup.read', 'backup.write'
];
useEffect(() => {
loadUtilisateurs();
}, []);
const loadUtilisateurs = async () => {
try {
setLoading(true);
// TODO: Remplacer par un appel API réel pour charger les utilisateurs depuis le backend
// Exemple: const response = await fetch('/api/admin/users');
// const utilisateursData = await response.json();
const mockUtilisateurs: Utilisateur[] = [];
// TODO: Remplacer par un appel API réel pour charger l'activité des utilisateurs
// Exemple: const response = await fetch('/api/admin/user-activities');
// const activitesData = await response.json();
const mockActivites: ActiviteUtilisateur[] = [];
setUtilisateurs(mockUtilisateurs);
setActivites(mockActivites);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les utilisateurs',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNew = () => {
setUtilisateur({
id: '',
nom: '',
prenom: '',
email: '',
telephone: '',
role: 'USER',
departement: '',
statut: 'ACTIF',
actif: true,
derniereConnexion: new Date(),
dateCreation: new Date(),
dateModification: new Date(),
permissions: [],
adresse: '',
numeroEmploye: '',
tentativesConnexion: 0,
derniereMiseAJourMotDePasse: new Date(),
motDePasseExpire: false,
deuxFacteursActive: false,
sessionActive: false,
heuresConnexion: 0,
dernierAction: '',
preferences: {
theme: 'light',
langue: 'fr',
notifications: true,
timezone: 'Europe/Paris'
}
});
setPassword('');
setConfirmPassword('');
setSubmitted(false);
setUtilisateurDialog(true);
};
const editUtilisateur = (utilisateur: Utilisateur) => {
setUtilisateur({ ...utilisateur });
setPassword('');
setConfirmPassword('');
setUtilisateurDialog(true);
};
const confirmDelete = (utilisateur: Utilisateur) => {
setUtilisateur(utilisateur);
setDeleteDialog(true);
};
const deleteUtilisateur = () => {
const updatedUtilisateurs = utilisateurs.filter(u => u.id !== utilisateur.id);
setUtilisateurs(updatedUtilisateurs);
setDeleteDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur supprimé',
life: 3000
});
};
const saveUtilisateur = () => {
setSubmitted(true);
if (utilisateur.nom.trim() && utilisateur.prenom.trim() && utilisateur.email.trim()) {
if (!utilisateur.id && (!password || password !== confirmPassword)) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Mot de passe requis et doit correspondre',
life: 3000
});
return;
}
let updatedUtilisateurs = [...utilisateurs];
if (utilisateur.id) {
// Mise à jour
const index = utilisateurs.findIndex(u => u.id === utilisateur.id);
updatedUtilisateurs[index] = { ...utilisateur, dateModification: new Date() };
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur mis à jour',
life: 3000
});
} else {
// Création
const newUtilisateur = {
...utilisateur,
id: Date.now().toString(),
numeroEmploye: `EMP${String(utilisateurs.length + 1).padStart(3, '0')}`,
dateCreation: new Date(),
dateModification: new Date()
};
updatedUtilisateurs.push(newUtilisateur);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur créé',
life: 3000
});
}
setUtilisateurs(updatedUtilisateurs);
setUtilisateurDialog(false);
}
};
const resetPassword = (utilisateur: Utilisateur) => {
setUtilisateur(utilisateur);
setPassword('');
setConfirmPassword('');
setResetPasswordDialog(true);
};
const confirmResetPassword = () => {
if (password && password === confirmPassword) {
const updatedUtilisateurs = utilisateurs.map(u =>
u.id === utilisateur.id
? { ...u, derniereMiseAJourMotDePasse: new Date(), motDePasseExpire: false, tentativesConnexion: 0 }
: u
);
setUtilisateurs(updatedUtilisateurs);
setResetPasswordDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Mot de passe réinitialisé',
life: 3000
});
} else {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Les mots de passe ne correspondent pas',
life: 3000
});
}
};
const suspendUser = (utilisateur: Utilisateur) => {
const updatedUtilisateurs = utilisateurs.map(u =>
u.id === utilisateur.id
? { ...u, statut: u.statut === 'SUSPENDU' ? 'ACTIF' : 'SUSPENDU' } as Utilisateur
: u
);
setUtilisateurs(updatedUtilisateurs);
toast.current?.show({
severity: 'info',
summary: 'Statut modifié',
detail: `Utilisateur ${utilisateur.statut === 'SUSPENDU' ? 'réactivé' : 'suspendu'}`,
life: 3000
});
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvel Utilisateur"
icon="pi pi-plus"
severity="success"
onClick={openNew}
/>
<Button
label="Activité"
icon="pi pi-list"
onClick={() => setActiviteDialog(true)}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
onClick={exportCSV}
/>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
};
const actionBodyTemplate = (rowData: Utilisateur) => {
return (
<ActionButtonGroup>
<EditButton
onClick={() => editUtilisateur(rowData)}
tooltip="Modifier"
/>
<ActionButton
icon="pi pi-key"
color="orange"
onClick={() => resetPassword(rowData)}
tooltip="Réinitialiser mot de passe"
/>
<ActionButton
icon={rowData.statut === 'SUSPENDU' ? 'pi pi-check' : 'pi pi-ban'}
color={rowData.statut === 'SUSPENDU' ? 'blue' : 'red'}
onClick={() => suspendUser(rowData)}
tooltip={rowData.statut === 'SUSPENDU' ? 'Réactiver' : 'Suspendre'}
/>
<DeleteButton
onClick={() => confirmDelete(rowData)}
tooltip="Supprimer"
/>
</ActionButtonGroup>
);
};
const statusBodyTemplate = (rowData: Utilisateur) => {
let severity: "success" | "warning" | "danger" | "info" = 'success';
let label: string = rowData.statut;
switch (rowData.statut) {
case 'ACTIF':
severity = 'success';
label = 'Actif';
break;
case 'INACTIF':
severity = 'warning';
label = 'Inactif';
break;
case 'SUSPENDU':
severity = 'danger';
label = 'Suspendu';
break;
case 'CONGE':
severity = 'info';
label = 'Congé';
break;
}
return <Tag value={label} severity={severity} />;
};
const roleBodyTemplate = (rowData: Utilisateur) => {
const roleLabels = {
'ADMIN': 'Administrateur',
'MANAGER': 'Manager',
'USER': 'Utilisateur',
'VIEWER': 'Lecture seule'
};
return roleLabels[rowData.role] || rowData.role;
};
const sessionBodyTemplate = (rowData: Utilisateur) => {
return (
<div className="flex align-items-center gap-2">
<i className={`pi ${rowData.sessionActive ? 'pi-circle-fill text-green-500' : 'pi-circle text-gray-400'}`}></i>
<span>{rowData.sessionActive ? 'En ligne' : 'Hors ligne'}</span>
</div>
);
};
const securityBodyTemplate = (rowData: Utilisateur) => {
return (
<div className="flex gap-1">
{rowData.deuxFacteursActive && <Chip label="2FA" className="p-0 text-xs" style={{ fontSize: '0.75rem', padding: '0.25rem 0.5rem' }} />}
{rowData.motDePasseExpire && <Chip label="PWD" className="p-0 text-xs" style={{ fontSize: '0.75rem', padding: '0.25rem 0.5rem' }} />}
{rowData.tentativesConnexion > 0 && <Badge value={rowData.tentativesConnexion} severity="warning" />}
</div>
);
};
const avatarBodyTemplate = (rowData: Utilisateur) => {
return (
<div className="flex align-items-center gap-2">
<Avatar
label={`${rowData.prenom[0]}${rowData.nom[0]}`}
size="normal"
shape="circle"
style={{ backgroundColor: '#2196F3', color: 'white' }}
/>
<div>
<div className="font-semibold">{rowData.prenom} {rowData.nom}</div>
<div className="text-sm text-color-secondary">{rowData.numeroEmploye}</div>
</div>
</div>
);
};
const dernierActionBodyTemplate = (rowData: Utilisateur) => {
const diffTime = Math.abs(new Date().getTime() - rowData.derniereConnexion.getTime());
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
const timeAgo = diffDays > 0 ? `Il y a ${diffDays}j` : `Il y a ${diffHours}h`;
return (
<div>
<div className="text-sm">{rowData.dernierAction}</div>
<div className="text-xs text-color-secondary">{timeAgo}</div>
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Gestion des Utilisateurs</h5>
<div className="flex gap-2">
<Badge value={utilisateurs.filter(u => u.sessionActive).length} className="mr-2" />
<span className="text-sm">utilisateurs en ligne</span>
</div>
</div>
);
// Statistiques pour le dashboard
const totalUtilisateurs = utilisateurs.length;
const utilisateursActifs = utilisateurs.filter(u => u.statut === 'ACTIF').length;
const utilisateursEnLigne = utilisateurs.filter(u => u.sessionActive).length;
const utilisateursAvec2FA = utilisateurs.filter(u => u.deuxFacteursActive).length;
const rolesData = {
labels: ['Admin', 'Manager', 'User', 'Viewer'],
datasets: [
{
data: [
utilisateurs.filter(u => u.role === 'ADMIN').length,
utilisateurs.filter(u => u.role === 'MANAGER').length,
utilisateurs.filter(u => u.role === 'USER').length,
utilisateurs.filter(u => u.role === 'VIEWER').length
],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'],
hoverBackgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0']
}
]
};
const departementsData = {
labels: departements.map(d => d.label),
datasets: [
{
label: 'Utilisateurs',
data: departements.map(dept => utilisateurs.filter(u => u.departement === dept.value).length),
backgroundColor: '#36A2EB',
borderColor: '#36A2EB',
borderWidth: 1
}
]
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
<TabPanel header="Utilisateurs" leftIcon="pi pi-users mr-2">
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={utilisateurs}
selection={selectedUtilisateurs}
onSelectionChange={(e) => setSelectedUtilisateurs(e.value)}
selectionMode="multiple"
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} utilisateurs"
globalFilter={globalFilter}
emptyMessage="Aucun utilisateur trouvé."
header={header}
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="utilisateur" header="Utilisateur" body={avatarBodyTemplate} />
<Column field="email" header="Email" sortable />
<Column field="role" header="Rôle" body={roleBodyTemplate} sortable />
<Column field="departement" header="Département" sortable />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
<Column field="session" header="Session" body={sessionBodyTemplate} />
<Column field="derniereConnexion" header="Dernière activité" body={dernierActionBodyTemplate} sortable />
<Column field="securite" header="Sécurité" body={securityBodyTemplate} />
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
</DataTable>
</TabPanel>
<TabPanel header="Statistiques" leftIcon="pi pi-chart-bar mr-2">
<div className="grid">
{/* Indicateurs principaux */}
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-primary mb-2">
<i className="pi pi-users"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{totalUtilisateurs}
</div>
<div className="text-lg text-color-secondary">
Total Utilisateurs
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-check-circle"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{utilisateursActifs}
</div>
<div className="text-lg text-color-secondary">
Utilisateurs Actifs
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-orange-500 mb-2">
<i className="pi pi-circle-fill"></i>
</div>
<div className="text-3xl font-bold text-orange-500 mb-1">
{utilisateursEnLigne}
</div>
<div className="text-lg text-color-secondary">
En Ligne
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-purple-500 mb-2">
<i className="pi pi-shield"></i>
</div>
<div className="text-3xl font-bold text-purple-500 mb-1">
{utilisateursAvec2FA}
</div>
<div className="text-lg text-color-secondary">
Avec 2FA
</div>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 md:col-6">
<Card title="Répartition par Rôle">
<Chart type="doughnut" data={rolesData} style={{ height: '300px' }} />
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Répartition par Département">
<Chart type="bar" data={departementsData} style={{ height: '300px' }} />
</Card>
</div>
</div>
</TabPanel>
</TabView>
{/* Dialog utilisateur */}
<Dialog
visible={utilisateurDialog}
style={{ width: '900px' }}
header="Détails de l'utilisateur"
modal
className="p-fluid"
footer={
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setUtilisateurDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveUtilisateur} />
</div>
}
onHide={() => setUtilisateurDialog(false)}
>
<TabView>
<TabPanel header="Informations générales">
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="prenom">Prénom *</label>
<InputText
id="prenom"
value={utilisateur.prenom}
onChange={(e) => setUtilisateur({...utilisateur, prenom: e.target.value})}
required
className={submitted && !utilisateur.prenom ? 'p-invalid' : ''}
/>
{submitted && !utilisateur.prenom && <small className="p-invalid">Le prénom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="nom">Nom *</label>
<InputText
id="nom"
value={utilisateur.nom}
onChange={(e) => setUtilisateur({...utilisateur, nom: e.target.value})}
required
className={submitted && !utilisateur.nom ? 'p-invalid' : ''}
/>
{submitted && !utilisateur.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="email">Email *</label>
<InputText
id="email"
value={utilisateur.email}
onChange={(e) => setUtilisateur({...utilisateur, email: e.target.value})}
required
className={submitted && !utilisateur.email ? 'p-invalid' : ''}
/>
{submitted && !utilisateur.email && <small className="p-invalid">L'email est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="telephone">Téléphone</label>
<InputText
id="telephone"
value={utilisateur.telephone}
onChange={(e) => setUtilisateur({...utilisateur, telephone: e.target.value})}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="role">Rôle</label>
<Dropdown
id="role"
value={utilisateur.role}
options={roles}
onChange={(e) => setUtilisateur({...utilisateur, role: e.value})}
placeholder="Sélectionnez un rôle"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="departement">Département</label>
<Dropdown
id="departement"
value={utilisateur.departement}
options={departements}
onChange={(e) => setUtilisateur({...utilisateur, departement: e.value})}
placeholder="Sélectionnez un département"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="statut">Statut</label>
<Dropdown
id="statut"
value={utilisateur.statut}
options={statuts}
onChange={(e) => setUtilisateur({...utilisateur, statut: e.value})}
/>
</div>
<div className="field col-12">
<label htmlFor="adresse">Adresse</label>
<InputText
id="adresse"
value={utilisateur.adresse}
onChange={(e) => setUtilisateur({...utilisateur, adresse: e.target.value})}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateNaissance">Date de naissance</label>
<Calendar
id="dateNaissance"
value={utilisateur.dateNaissance}
onChange={(e) => setUtilisateur({...utilisateur, dateNaissance: e.value || undefined})}
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="manager">Manager</label>
<InputText
id="manager"
value={utilisateur.manager || ''}
onChange={(e) => setUtilisateur({...utilisateur, manager: e.target.value})}
/>
</div>
{!utilisateur.id && (
<>
<div className="field col-12 md:col-6">
<label htmlFor="password">Mot de passe *</label>
<Password
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className={submitted && !password ? 'p-invalid' : ''}
/>
{submitted && !password && <small className="p-invalid">Le mot de passe est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="confirmPassword">Confirmer le mot de passe *</label>
<Password
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className={submitted && password !== confirmPassword ? 'p-invalid' : ''}
/>
{submitted && password !== confirmPassword && <small className="p-invalid">Les mots de passe ne correspondent pas.</small>}
</div>
</>
)}
</div>
</TabPanel>
<TabPanel header="Permissions">
<div className="grid">
{availablePermissions.map(permission => (
<div key={permission} className="col-12 md:col-6">
<div className="flex align-items-center">
<Checkbox
checked={utilisateur.permissions.includes(permission)}
onChange={(e) => {
const newPermissions = e.checked
? [...utilisateur.permissions, permission]
: utilisateur.permissions.filter(p => p !== permission);
setUtilisateur({...utilisateur, permissions: newPermissions});
}}
/>
<label className="ml-2">{permission}</label>
</div>
</div>
))}
</div>
</TabPanel>
<TabPanel header="Sécurité">
<div className="formgrid grid">
<div className="field col-12">
<div className="flex align-items-center">
<Checkbox
checked={utilisateur.deuxFacteursActive}
onChange={(e) => setUtilisateur({...utilisateur, deuxFacteursActive: e.checked || false})}
/>
<label className="ml-2">Authentification à deux facteurs (2FA)</label>
</div>
</div>
<div className="field col-12">
<div className="flex align-items-center">
<Checkbox
checked={utilisateur.motDePasseExpire}
onChange={(e) => setUtilisateur({...utilisateur, motDePasseExpire: e.checked || false})}
/>
<label className="ml-2">Forcer le changement de mot de passe</label>
</div>
</div>
<div className="field col-12 md:col-6">
<label>Tentatives de connexion échouées</label>
<InputText value={utilisateur.tentativesConnexion.toString()} disabled />
</div>
<div className="field col-12 md:col-6">
<label>Dernière mise à jour du mot de passe</label>
<InputText value={utilisateur.derniereMiseAJourMotDePasse.toLocaleDateString()} disabled />
</div>
</div>
</TabPanel>
</TabView>
</Dialog>
{/* Dialog réinitialisation mot de passe */}
<Dialog
visible={resetPasswordDialog}
style={{ width: '450px' }}
header="Réinitialiser le mot de passe"
modal
className="p-fluid"
footer={
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setResetPasswordDialog(false)} />
<Button label="Réinitialiser" icon="pi pi-check" onClick={confirmResetPassword} />
</div>
}
onHide={() => setResetPasswordDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<p>Réinitialiser le mot de passe pour <strong>{utilisateur.prenom} {utilisateur.nom}</strong></p>
</div>
<div className="field col-12">
<label htmlFor="newPassword">Nouveau mot de passe</label>
<Password
id="newPassword"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="field col-12">
<label htmlFor="confirmNewPassword">Confirmer le nouveau mot de passe</label>
<Password
id="confirmNewPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
</div>
</Dialog>
{/* Dialog suppression */}
<Dialog
visible={deleteDialog}
style={{ width: '450px' }}
header="Confirmer la suppression"
modal
footer={
<div className="flex justify-content-end gap-2">
<Button label="Non" icon="pi pi-times" text onClick={() => setDeleteDialog(false)} />
<Button label="Oui" icon="pi pi-check" onClick={deleteUtilisateur} />
</div>
}
onHide={() => setDeleteDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{utilisateur && (
<span>
Êtes-vous sûr de vouloir supprimer l'utilisateur <b>{utilisateur.prenom} {utilisateur.nom}</b> ?
</span>
)}
</div>
</Dialog>
{/* Dialog activité */}
<Dialog
visible={activiteDialog}
style={{ width: '80vw' }}
header="Activité des utilisateurs"
modal
footer={
<Button label="Fermer" icon="pi pi-times" onClick={() => setActiviteDialog(false)} />
}
onHide={() => setActiviteDialog(false)}
>
<DataTable
value={activites}
paginator
rows={15}
loading={loading}
>
<Column field="timestamp" header="Date/Heure" body={(rowData) => rowData.timestamp.toLocaleString()} sortable />
<Column field="utilisateurId" header="Utilisateur" body={(rowData) => {
const user = utilisateurs.find(u => u.id === rowData.utilisateurId);
return user ? `${user.prenom} ${user.nom}` : 'Utilisateur inconnu';
}} />
<Column field="action" header="Action" />
<Column field="module" header="Module" />
<Column field="details" header="Détails" />
<Column field="adresseIP" header="IP" />
<Column field="navigateur" header="Navigateur" />
</DataTable>
</Dialog>
</Card>
</div>
</div>
);
};
const ProtectedUtilisateursPage = () => {
return (
<ProtectedRoute requiredRoles={['SUPER_ADMIN', 'ADMIN']}>
<UtilisateursPage />
</ProtectedRoute>
);
};
export default ProtectedUtilisateursPage;