Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,379 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Dialog } from 'primereact/dialog';
import { Dropdown } from 'primereact/dropdown';
import { MultiSelect } from 'primereact/multiselect';
import { Tag } from 'primereact/tag';
import { Divider } from 'primereact/divider';
import clientService from '../../../../services/clientService';
import userService from '../../../../services/userService';
import type { Client } from '../../../../types/btp';
import type { User } from '../../../../types/auth';
interface ClientGestionnaire {
client: Client;
gestionnairePrincipal?: User;
gestionnairesSecondaires: User[];
}
const AttributionsPage = () => {
const [clients, setClients] = useState<Client[]>([]);
const [gestionnaires, setGestionnaires] = useState<User[]>([]);
const [attributions, setAttributions] = useState<ClientGestionnaire[]>([]);
const [loading, setLoading] = useState(true);
const [attributionDialog, setAttributionDialog] = useState(false);
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
const [selectedGestionnairePrincipal, setSelectedGestionnairePrincipal] = useState<User | null>(null);
const [selectedGestionnairesSecondaires, setSelectedGestionnairesSecondaires] = useState<User[]>([]);
const toast = useRef<Toast>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Charger les clients
const clientsData = await clientService.getAll();
setClients(clientsData);
// Charger les gestionnaires depuis le service
const gestionnairesData = await userService.getGestionnaires();
setGestionnaires(gestionnairesData);
// Construire les attributions
const attributionsData = clientsData.map(client => ({
client,
gestionnairePrincipal: gestionnairesData.find(g => g.id === client.gestionnairePrincipalId),
gestionnairesSecondaires: gestionnairesData.filter(g =>
client.gestionnairesSecondaires?.includes(g.id)
)
}));
setAttributions(attributionsData);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const openAttributionDialog = (client: Client) => {
setSelectedClient(client);
// Pré-remplir les sélections actuelles
const gestionnairePrincipal = gestionnaires.find(g => g.id === client.gestionnairePrincipalId);
setSelectedGestionnairePrincipal(gestionnairePrincipal || null);
const gestionnairesSecondaires = gestionnaires.filter(g =>
client.gestionnairesSecondaires?.includes(g.id)
);
setSelectedGestionnairesSecondaires(gestionnairesSecondaires);
setAttributionDialog(true);
};
const saveAttribution = async () => {
if (!selectedClient) return;
try {
const updatedClient = {
...selectedClient,
gestionnairePrincipalId: selectedGestionnairePrincipal?.id,
gestionnairesSecondaires: selectedGestionnairesSecondaires.map(g => g.id)
};
// Mise à jour côté serveur
await clientService.update(selectedClient.id, updatedClient);
// Mettre à jour localement
const updatedClients = clients.map(c =>
c.id === selectedClient.id ? updatedClient : c
);
setClients(updatedClients);
// Mettre à jour les attributions
const updatedAttributions = attributions.map(a =>
a.client.id === selectedClient.id
? {
...a,
client: updatedClient,
gestionnairePrincipal: selectedGestionnairePrincipal || undefined,
gestionnairesSecondaires: selectedGestionnairesSecondaires
}
: a
);
setAttributions(updatedAttributions);
setAttributionDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Attribution mise à jour',
life: 3000
});
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de sauvegarder l\'attribution',
life: 3000
});
}
};
const hideDialog = () => {
setAttributionDialog(false);
setSelectedClient(null);
setSelectedGestionnairePrincipal(null);
setSelectedGestionnairesSecondaires([]);
};
const leftToolbarTemplate = () => {
return (
<div className="my-2">
<Button
label="Actualiser"
icon="pi pi-refresh"
className="mr-2"
onClick={loadData}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex align-items-center">
<span className="text-color-secondary mr-2">
{attributions.filter(a => a.gestionnairePrincipal).length} / {attributions.length} clients attribués
</span>
</div>
);
};
const actionBodyTemplate = (rowData: ClientGestionnaire) => {
return (
<Button
icon="pi pi-pencil"
className="p-button-rounded p-button-text p-button-info"
tooltip="Attribuer"
onClick={() => openAttributionDialog(rowData.client)}
/>
);
};
const clientBodyTemplate = (rowData: ClientGestionnaire) => {
return (
<div className="flex flex-column">
<span className="font-medium">{`${rowData.client.prenom} ${rowData.client.nom}`}</span>
{rowData.client.entreprise && (
<span className="text-sm text-color-secondary">{rowData.client.entreprise}</span>
)}
</div>
);
};
const gestionnairePrincipalBodyTemplate = (rowData: ClientGestionnaire) => {
if (!rowData.gestionnairePrincipal) {
return (
<Tag
value="Non attribué"
severity="danger"
className="text-sm"
/>
);
}
return (
<div className="flex flex-column">
<span className="font-medium">
{`${rowData.gestionnairePrincipal.prenom} ${rowData.gestionnairePrincipal.nom}`}
</span>
<span className="text-sm text-color-secondary">
{rowData.gestionnairePrincipal.email}
</span>
</div>
);
};
const gestionnairesSecondairesBodyTemplate = (rowData: ClientGestionnaire) => {
if (rowData.gestionnairesSecondaires.length === 0) {
return <span className="text-color-secondary">Aucun</span>;
}
return (
<div className="flex flex-column gap-1">
{rowData.gestionnairesSecondaires.map(gestionnaire => (
<Tag
key={gestionnaire.id}
value={`${gestionnaire.prenom} ${gestionnaire.nom}`}
severity="info"
className="text-sm"
/>
))}
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Attribution des Clients aux Gestionnaires</h5>
</div>
);
const attributionDialogFooter = (
<>
<Button
label="Annuler"
icon="pi pi-times"
text
onClick={hideDialog}
/>
<Button
label="Sauvegarder"
icon="pi pi-check"
text
onClick={saveAttribution}
/>
</>
);
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
value={attributions}
dataKey="client.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} attributions"
emptyMessage="Aucune attribution trouvée."
header={header}
responsiveLayout="scroll"
loading={loading}
>
<Column
header="Client"
body={clientBodyTemplate}
sortable
headerStyle={{ minWidth: '15rem' }}
/>
<Column
header="Gestionnaire Principal"
body={gestionnairePrincipalBodyTemplate}
headerStyle={{ minWidth: '15rem' }}
/>
<Column
header="Gestionnaires Secondaires"
body={gestionnairesSecondairesBodyTemplate}
headerStyle={{ minWidth: '12rem' }}
/>
<Column
body={actionBodyTemplate}
headerStyle={{ minWidth: '8rem' }}
/>
</DataTable>
<Dialog
visible={attributionDialog}
style={{ width: '600px' }}
header="Attribution des Gestionnaires"
modal
className="p-fluid"
footer={attributionDialogFooter}
onHide={hideDialog}
>
{selectedClient && (
<div>
<div className="field mb-4">
<h6 className="text-primary">Client</h6>
<div className="p-3 surface-100 border-round">
<div className="font-medium text-lg">
{`${selectedClient.prenom} ${selectedClient.nom}`}
</div>
{selectedClient.entreprise && (
<div className="text-color-secondary">
{selectedClient.entreprise}
</div>
)}
</div>
</div>
<Divider />
<div className="field">
<label htmlFor="gestionnairePrincipal">
<strong>Gestionnaire Principal</strong>
</label>
<Dropdown
id="gestionnairePrincipal"
value={selectedGestionnairePrincipal}
options={gestionnaires}
onChange={(e) => setSelectedGestionnairePrincipal(e.value)}
optionLabel={(option) => `${option.prenom} ${option.nom}`}
placeholder="Sélectionnez un gestionnaire principal"
showClear
className="w-full"
/>
<small className="text-color-secondary">
Le gestionnaire principal est responsable de la relation client
</small>
</div>
<div className="field">
<label htmlFor="gestionnairesSecondaires">
<strong>Gestionnaires Secondaires</strong>
</label>
<MultiSelect
id="gestionnairesSecondaires"
value={selectedGestionnairesSecondaires}
options={gestionnaires.filter(g => g.id !== selectedGestionnairePrincipal?.id)}
onChange={(e) => setSelectedGestionnairesSecondaires(e.value)}
optionLabel={(option) => `${option.prenom} ${option.nom}`}
placeholder="Sélectionnez des gestionnaires secondaires"
className="w-full"
maxSelectedLabels={3}
/>
<small className="text-color-secondary">
Les gestionnaires secondaires peuvent consulter et collaborer sur les projets
</small>
</div>
</div>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default AttributionsPage;

File diff suppressed because it is too large Load Diff

112
app/(main)/admin/page.tsx Normal file
View File

@@ -0,0 +1,112 @@
'use client';
import React from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { useRouter } from 'next/navigation';
import RoleProtectedPage from '@/components/RoleProtectedPage';
const AdminDashboardContent = () => {
const router = useRouter();
const adminModules = [
{
title: 'Gestion des utilisateurs',
description: 'Créer, modifier et gérer les comptes utilisateurs',
icon: 'pi pi-users',
path: '/admin/utilisateurs',
color: 'bg-blue-500'
},
{
title: 'Gestion des rôles',
description: 'Configurer les rôles et permissions',
icon: 'pi pi-shield',
path: '/admin/roles',
color: 'bg-green-500'
},
{
title: 'Demandes d\'accès',
description: 'Traiter les demandes d\'accès en attente',
icon: 'pi pi-key',
path: '/admin/demandes-acces',
color: 'bg-orange-500'
},
{
title: 'Attributions',
description: 'Gérer les attributions de projets et équipes',
icon: 'pi pi-sitemap',
path: '/admin/attributions',
color: 'bg-purple-500'
},
{
title: 'Paramètres système',
description: 'Configuration générale de l\'application',
icon: 'pi pi-cog',
path: '/admin/parametres',
color: 'bg-gray-500'
},
{
title: 'Sauvegarde',
description: 'Gestion des sauvegardes et restaurations',
icon: 'pi pi-database',
path: '/admin/sauvegarde',
color: 'bg-red-500'
}
];
return (
<div className="grid">
<div className="col-12">
<div className="card">
<h1 className="text-3xl font-bold text-900 mb-3">
<i className="pi pi-shield mr-3 text-primary"></i>
Administration
</h1>
<p className="text-600 text-lg mb-5">
Panneau d'administration pour la gestion complète de l'application BTPXpress
</p>
</div>
</div>
{adminModules.map((module, index) => (
<div key={index} className="col-12 md:col-6 lg:col-4">
<Card className="h-full">
<div className="text-center">
<div
className={`${module.color} text-white border-round inline-flex align-items-center justify-content-center mb-3`}
style={{ width: '60px', height: '60px' }}
>
<i className={`${module.icon} text-2xl`}></i>
</div>
<h3 className="text-xl font-semibold text-900 mb-2">
{module.title}
</h3>
<p className="text-600 mb-4 line-height-3">
{module.description}
</p>
<Button
label="Accéder"
icon="pi pi-arrow-right"
onClick={() => router.push(module.path)}
className="w-full"
/>
</div>
</Card>
</div>
))}
</div>
);
};
const AdminDashboard = () => {
return (
<RoleProtectedPage
requiredPage="ADMIN"
fallbackMessage="Seuls les administrateurs peuvent accéder à cette section."
>
<AdminDashboardContent />
</RoleProtectedPage>
);
};
export default AdminDashboard;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,711 @@
'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 { InputTextarea } from 'primereact/inputtextarea';
import { Tag } from 'primereact/tag';
import { Checkbox } from 'primereact/checkbox';
import { Tree } from 'primereact/tree';
import { TabView, TabPanel } from 'primereact/tabview';
import { Panel } from 'primereact/panel';
import { Badge } from 'primereact/badge';
import { Chip } from 'primereact/chip';
interface Permission {
id: string;
nom: string;
description: string;
module: string;
action: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' | 'EXECUTE';
ressource: string;
cle: string;
}
interface Role {
id: string;
nom: string;
description: string;
type: 'SYSTEM' | 'CUSTOM';
permissions: string[];
utilisateursAssignes: number;
dateCreation: Date;
dateModification: Date;
actif: boolean;
couleur: string;
priorite: number;
heriteDe?: string;
}
interface ModulePermission {
module: string;
permissions: Permission[];
}
const RolesPermissionsPage = () => {
const [roles, setRoles] = useState<Role[]>([]);
const [permissions, setPermissions] = useState<Permission[]>([]);
const [modulesPermissions, setModulesPermissions] = useState<ModulePermission[]>([]);
const [selectedRoles, setSelectedRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [roleDialog, setRoleDialog] = useState(false);
const [permissionDialog, setPermissionDialog] = useState(false);
const [deleteRoleDialog, setDeleteRoleDialog] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const [role, setRole] = useState<Role>({
id: '',
nom: '',
description: '',
type: 'CUSTOM',
permissions: [],
utilisateursAssignes: 0,
dateCreation: new Date(),
dateModification: new Date(),
actif: true,
couleur: '#2196F3',
priorite: 0
});
const [permission, setPermission] = useState<Permission>({
id: '',
nom: '',
description: '',
module: '',
action: 'READ',
ressource: '',
cle: ''
});
const [submitted, setSubmitted] = useState(false);
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Role[]>>(null);
const modules = [
'Dashboard', 'Utilisateurs', 'Clients', 'Chantiers', 'Devis', 'Factures',
'Stock', 'Planning', 'Rapports', 'Administration', 'Système'
];
const actions = [
{ label: 'Créer', value: 'CREATE' },
{ label: 'Lire', value: 'READ' },
{ label: 'Modifier', value: 'UPDATE' },
{ label: 'Supprimer', value: 'DELETE' },
{ label: 'Exécuter', value: 'EXECUTE' }
];
const couleurs = [
{ label: 'Bleu', value: '#2196F3' },
{ label: 'Vert', value: '#4CAF50' },
{ label: 'Orange', value: '#FF9800' },
{ label: 'Rouge', value: '#F44336' },
{ label: 'Violet', value: '#9C27B0' },
{ label: 'Indigo', value: '#3F51B5' }
];
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// TODO: Remplacer par un appel API réel pour charger les permissions depuis le backend
// Exemple: const response = await fetch('/api/admin/permissions');
// const permissions = await response.json();
const mockPermissions: Permission[] = [];
// TODO: Remplacer par un appel API réel pour charger les rôles depuis le backend
// Exemple: const response = await fetch('/api/admin/roles');
// const roles = await response.json();
const mockRoles: Role[] = [];
// Groupement par modules
const groupedPermissions: ModulePermission[] = modules.map(module => ({
module,
permissions: mockPermissions.filter(p => p.module === module)
})).filter(group => group.permissions.length > 0);
setPermissions(mockPermissions);
setRoles(mockRoles);
setModulesPermissions(groupedPermissions);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNewRole = () => {
setRole({
id: '',
nom: '',
description: '',
type: 'CUSTOM',
permissions: [],
utilisateursAssignes: 0,
dateCreation: new Date(),
dateModification: new Date(),
actif: true,
couleur: '#2196F3',
priorite: 0
});
setSelectedPermissions([]);
setSubmitted(false);
setRoleDialog(true);
};
const editRole = (role: Role) => {
setRole({ ...role });
setSelectedPermissions([...role.permissions]);
setRoleDialog(true);
};
const confirmDeleteRole = (role: Role) => {
setRole(role);
setDeleteRoleDialog(true);
};
const deleteRole = () => {
if (role.type === 'SYSTEM') {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de supprimer un rôle système',
life: 3000
});
return;
}
const updatedRoles = roles.filter(r => r.id !== role.id);
setRoles(updatedRoles);
setDeleteRoleDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Rôle supprimé',
life: 3000
});
};
const saveRole = () => {
setSubmitted(true);
if (role.nom.trim() && role.description.trim()) {
let updatedRoles = [...roles];
const roleToSave = { ...role, permissions: selectedPermissions };
if (role.id) {
// Mise à jour
const index = roles.findIndex(r => r.id === role.id);
updatedRoles[index] = { ...roleToSave, dateModification: new Date() };
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Rôle mis à jour',
life: 3000
});
} else {
// Création
const newRole = {
...roleToSave,
id: Date.now().toString(),
dateCreation: new Date(),
dateModification: new Date()
};
updatedRoles.push(newRole);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Rôle créé',
life: 3000
});
}
setRoles(updatedRoles);
setRoleDialog(false);
}
};
const duplicateRole = (role: Role) => {
const newRole = {
...role,
id: Date.now().toString(),
nom: `${role.nom} (Copie)`,
type: 'CUSTOM' as const,
utilisateursAssignes: 0,
dateCreation: new Date(),
dateModification: new Date()
};
setRoles([...roles, newRole]);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Rôle dupliqué',
life: 3000
});
};
const onPermissionToggle = (permissionId: string, checked: boolean) => {
if (checked) {
setSelectedPermissions([...selectedPermissions, permissionId]);
} else {
setSelectedPermissions(selectedPermissions.filter(id => id !== permissionId));
}
};
const selectAllPermissionsForModule = (module: string, checked: boolean) => {
const modulePermissions = permissions.filter(p => p.module === module).map(p => p.id);
if (checked) {
const newPermissions = [...selectedPermissions];
modulePermissions.forEach(permId => {
if (!newPermissions.includes(permId)) {
newPermissions.push(permId);
}
});
setSelectedPermissions(newPermissions);
} else {
setSelectedPermissions(selectedPermissions.filter(id => !modulePermissions.includes(id)));
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouveau Rôle"
icon="pi pi-plus"
severity="success"
onClick={openNewRole}
/>
</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: Role) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-eye"
rounded
severity="info"
onClick={() => editRole(rowData)}
tooltip="Voir/Modifier"
/>
<Button
icon="pi pi-copy"
rounded
severity="success"
onClick={() => duplicateRole(rowData)}
tooltip="Dupliquer"
/>
{rowData.type === 'CUSTOM' && (
<Button
icon="pi pi-trash"
rounded
severity="danger"
onClick={() => confirmDeleteRole(rowData)}
tooltip="Supprimer"
/>
)}
</div>
);
};
const typeBodyTemplate = (rowData: Role) => {
return (
<Tag
value={rowData.type === 'SYSTEM' ? 'Système' : 'Personnalisé'}
severity={rowData.type === 'SYSTEM' ? 'danger' : 'success'}
/>
);
};
const utilisateursBodyTemplate = (rowData: Role) => {
return <Badge value={rowData.utilisateursAssignes} severity="info" />;
};
const permissionsBodyTemplate = (rowData: Role) => {
return <Badge value={rowData.permissions.length} />;
};
const couleurBodyTemplate = (rowData: Role) => {
return (
<div className="flex align-items-center gap-2">
<div
className="w-1rem h-1rem border-circle"
style={{ backgroundColor: rowData.couleur }}
></div>
<span>{rowData.nom}</span>
</div>
);
};
const prioriteBodyTemplate = (rowData: Role) => {
const getPriorityColor = (priority: number) => {
if (priority >= 90) return 'danger';
if (priority >= 70) return 'warning';
if (priority >= 50) return 'info';
return 'success';
};
return <Tag value={rowData.priorite} severity={getPriorityColor(rowData.priorite)} />;
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Gestion des Rôles et Permissions</h5>
<div className="flex gap-2">
<Badge value={roles.filter(r => r.actif).length} className="mr-2" />
<span className="text-sm">rôles actifs</span>
</div>
</div>
);
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
<TabPanel header="Rôles" leftIcon="pi pi-users mr-2">
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={roles}
selection={selectedRoles}
onSelectionChange={(e) => setSelectedRoles(e.value)}
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} rôles"
globalFilter={globalFilter}
emptyMessage="Aucun rôle trouvé."
header={header}
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="nom" header="Nom" body={couleurBodyTemplate} sortable />
<Column field="description" header="Description" />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="priorite" header="Priorité" body={prioriteBodyTemplate} sortable />
<Column field="permissions" header="Permissions" body={permissionsBodyTemplate} />
<Column field="utilisateursAssignes" header="Utilisateurs" body={utilisateursBodyTemplate} sortable />
<Column field="dateModification" header="Modifié le" body={(rowData) => rowData.dateModification.toLocaleDateString()} sortable />
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '8rem' }} />
</DataTable>
</TabPanel>
<TabPanel header="Permissions" leftIcon="pi pi-key mr-2">
<div className="grid">
{modulesPermissions.map((moduleGroup, index) => (
<div key={index} className="col-12 md:col-6 lg:col-4">
<Panel header={moduleGroup.module} className="mb-3">
<div className="grid">
{moduleGroup.permissions.map((permission) => (
<div key={permission.id} className="col-12">
<div className="flex align-items-center justify-content-between p-2 border-bottom-1 surface-border">
<div>
<div className="font-semibold">{permission.nom}</div>
<div className="text-sm text-color-secondary">{permission.description}</div>
<Chip
label={permission.action}
className="mt-1"
style={{ fontSize: '0.75rem' }}
/>
</div>
</div>
</div>
))}
</div>
</Panel>
</div>
))}
</div>
</TabPanel>
<TabPanel header="Matrice" leftIcon="pi pi-table mr-2">
<Card title="Matrice Rôles-Permissions">
<div className="overflow-auto">
<table className="w-full">
<thead>
<tr>
<th className="text-left p-2 border-bottom-1 surface-border">Permission</th>
{roles.map(role => (
<th key={role.id} className="text-center p-2 border-bottom-1 surface-border">
<div className="flex flex-column align-items-center gap-1">
<div
className="w-1rem h-1rem border-circle"
style={{ backgroundColor: role.couleur }}
></div>
<span className="text-xs">{role.nom}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{modulesPermissions.map((moduleGroup, moduleIndex) => (
<React.Fragment key={moduleIndex}>
<tr>
<td colSpan={roles.length + 1} className="p-2 font-bold surface-100">
{moduleGroup.module}
</td>
</tr>
{moduleGroup.permissions.map((permission) => (
<tr key={permission.id}>
<td className="p-2 border-bottom-1 surface-border">
<div className="font-semibold">{permission.nom}</div>
<div className="text-xs text-color-secondary">{permission.action}</div>
</td>
{roles.map(role => (
<td key={role.id} className="text-center p-2 border-bottom-1 surface-border">
{role.permissions.includes(permission.id) ? (
<i className="pi pi-check text-green-500"></i>
) : (
<i className="pi pi-times text-red-500"></i>
)}
</td>
))}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
</Card>
</TabPanel>
</TabView>
{/* Dialog rôle */}
<Dialog
visible={roleDialog}
style={{ width: '900px' }}
header="Détails du rôle"
modal
className="p-fluid"
footer={
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setRoleDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveRole} />
</div>
}
onHide={() => setRoleDialog(false)}
>
<TabView>
<TabPanel header="Informations générales">
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="nom">Nom du rôle *</label>
<InputText
id="nom"
value={role.nom}
onChange={(e) => setRole({...role, nom: e.target.value})}
required
className={submitted && !role.nom ? 'p-invalid' : ''}
/>
{submitted && !role.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="couleur">Couleur</label>
<Dropdown
id="couleur"
value={role.couleur}
options={couleurs}
onChange={(e) => setRole({...role, couleur: e.value})}
itemTemplate={(option) => (
<div className="flex align-items-center gap-2">
<div
className="w-1rem h-1rem border-circle"
style={{ backgroundColor: option.value }}
></div>
<span>{option.label}</span>
</div>
)}
/>
</div>
<div className="field col-12">
<label htmlFor="description">Description *</label>
<InputTextarea
id="description"
value={role.description}
onChange={(e) => setRole({...role, description: e.target.value})}
rows={3}
required
className={submitted && !role.description ? 'p-invalid' : ''}
/>
{submitted && !role.description && <small className="p-invalid">La description est requise.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="priorite">Priorité (0-100)</label>
<InputText
id="priorite"
type="number"
min="0"
max="100"
value={role.priorite.toString()}
onChange={(e) => setRole({...role, priorite: parseInt(e.target.value) || 0})}
/>
</div>
<div className="field col-12 md:col-6">
<label>Type de rôle</label>
<div className="flex align-items-center">
<Tag
value={role.type === 'SYSTEM' ? 'Système' : 'Personnalisé'}
severity={role.type === 'SYSTEM' ? 'danger' : 'success'}
/>
</div>
</div>
</div>
</TabPanel>
<TabPanel header="Permissions">
<div className="grid">
{modulesPermissions.map((moduleGroup, index) => (
<div key={index} className="col-12">
<Panel
header={
<div className="flex align-items-center justify-content-between w-full">
<span>{moduleGroup.module}</span>
<Checkbox
checked={moduleGroup.permissions.every(p => selectedPermissions.includes(p.id))}
onChange={(e) => selectAllPermissionsForModule(moduleGroup.module, e.checked || false)}
/>
</div>
}
className="mb-3"
>
<div className="grid">
{moduleGroup.permissions.map((permission) => (
<div key={permission.id} className="col-12 md:col-6">
<div className="flex align-items-center">
<Checkbox
checked={selectedPermissions.includes(permission.id)}
onChange={(e) => onPermissionToggle(permission.id, e.checked || false)}
/>
<div className="ml-2">
<div className="font-semibold">{permission.nom}</div>
<div className="text-sm text-color-secondary">{permission.description}</div>
<Chip
label={permission.action}
className="mt-1"
style={{ fontSize: '0.75rem' }}
/>
</div>
</div>
</div>
))}
</div>
</Panel>
</div>
))}
</div>
<div className="mt-3 p-3 surface-100 border-round">
<div className="font-semibold mb-2">Résumé des permissions sélectionnées:</div>
<Badge value={selectedPermissions.length} className="mr-2" />
<span>permissions sélectionnées</span>
</div>
</TabPanel>
</TabView>
</Dialog>
{/* Dialog suppression */}
<Dialog
visible={deleteRoleDialog}
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={() => setDeleteRoleDialog(false)} />
<Button label="Oui" icon="pi pi-check" onClick={deleteRole} />
</div>
}
onHide={() => setDeleteRoleDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{role && (
<span>
Êtes-vous sûr de vouloir supprimer le rôle <b>{role.nom}</b> ?
{role.utilisateursAssignes > 0 && (
<div className="mt-2">
<small className="text-orange-600">
Attention: {role.utilisateursAssignes} utilisateur(s) ont ce rôle assigné.
</small>
</div>
)}
</span>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
const ProtectedRolesPermissionsPage = () => {
return (
<ProtectedRoute requiredRoles={['SUPER_ADMIN', 'ADMIN']}>
<RolesPermissionsPage />
</ProtectedRoute>
);
};
export default ProtectedRolesPermissionsPage;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,885 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
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 { Tag } from 'primereact/tag';
import { Dialog } from 'primereact/dialog';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { InputSwitch } from 'primereact/inputswitch';
import { InputNumber } from 'primereact/inputnumber';
import { ProgressBar } from 'primereact/progressbar';
import { FileUpload } from 'primereact/fileupload';
import { Checkbox } from 'primereact/checkbox';
import { RadioButton } from 'primereact/radiobutton';
import { Timeline } from 'primereact/timeline';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { Divider } from 'primereact/divider';
import { Panel } from 'primereact/panel';
import { Badge } from 'primereact/badge';
import { formatDate, formatDateTime } from '../../../../utils/formatters';
interface Backup {
id: string;
nom: string;
type: 'MANUEL' | 'AUTOMATIQUE' | 'PLANIFIE';
statut: 'EN_COURS' | 'COMPLETE' | 'ECHOUE' | 'PARTIEL';
dateCreation: Date;
taille: number;
duree: number;
fichiers: number;
destination: string;
chiffre: boolean;
compresse: boolean;
notes?: string;
utilisateur: string;
prochaineSauvegarde?: Date;
}
interface BackupSchedule {
id: string;
nom: string;
actif: boolean;
frequence: 'HORAIRE' | 'QUOTIDIEN' | 'HEBDOMADAIRE' | 'MENSUEL';
heure: Date;
joursSelection?: number[];
retention: number;
destination: string;
chiffrement: boolean;
compression: boolean;
inclureBase: boolean;
inclureFichiers: boolean;
inclureConfig: boolean;
}
interface RestorePoint {
id: string;
backupId: string;
nom: string;
date: Date;
type: string;
statut: 'DISPONIBLE' | 'CORROMPU' | 'EXPIRE';
taille: number;
}
const SauvegardePage = () => {
const [backups, setBackups] = useState<Backup[]>([]);
const [schedules, setSchedules] = useState<BackupSchedule[]>([]);
const [restorePoints, setRestorePoints] = useState<RestorePoint[]>([]);
const [loading, setLoading] = useState(false);
const [backupInProgress, setBackupInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [globalFilter, setGlobalFilter] = useState('');
const [scheduleDialog, setScheduleDialog] = useState(false);
const [restoreDialog, setRestoreDialog] = useState(false);
const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null);
const [selectedSchedule, setSelectedSchedule] = useState<BackupSchedule | null>(null);
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const [newSchedule, setNewSchedule] = useState<BackupSchedule>({
id: '',
nom: '',
actif: true,
frequence: 'QUOTIDIEN',
heure: new Date(),
retention: 30,
destination: 'local',
chiffrement: true,
compression: true,
inclureBase: true,
inclureFichiers: true,
inclureConfig: true
});
const frequenceOptions = [
{ label: 'Toutes les heures', value: 'HORAIRE' },
{ label: 'Quotidien', value: 'QUOTIDIEN' },
{ label: 'Hebdomadaire', value: 'HEBDOMADAIRE' },
{ label: 'Mensuel', value: 'MENSUEL' }
];
const destinationOptions = [
{ label: 'Stockage local', value: 'local' },
{ label: 'Google Drive', value: 'google' },
{ label: 'Dropbox', value: 'dropbox' },
{ label: 'AWS S3', value: 's3' },
{ label: 'Serveur FTP', value: 'ftp' }
];
const joursSemaine = [
{ label: 'Lundi', value: 1 },
{ label: 'Mardi', value: 2 },
{ label: 'Mercredi', value: 3 },
{ label: 'Jeudi', value: 4 },
{ label: 'Vendredi', value: 5 },
{ label: 'Samedi', value: 6 },
{ label: 'Dimanche', value: 0 }
];
useEffect(() => {
loadBackupData();
}, []);
const loadBackupData = () => {
setLoading(true);
// TODO: Remplacer par un appel API réel pour charger l'historique des sauvegardes
// Exemple: const response = await fetch('/api/admin/backups');
// const backups = await response.json();
const mockBackups: Backup[] = [];
// TODO: Remplacer par un appel API réel pour charger les planifications de sauvegarde
// Exemple: const response = await fetch('/api/admin/backup-schedules');
// const schedules = await response.json();
const mockSchedules: BackupSchedule[] = [];
// TODO: Remplacer par un appel API réel pour charger les points de restauration
// Exemple: const response = await fetch('/api/admin/restore-points');
// const restorePoints = await response.json();
const mockRestorePoints: RestorePoint[] = [];
setTimeout(() => {
setBackups(mockBackups);
setSchedules(mockSchedules);
setRestorePoints(mockRestorePoints);
setLoading(false);
}, 1000);
};
const startManualBackup = () => {
confirmDialog({
message: 'Voulez-vous lancer une sauvegarde manuelle maintenant ?',
header: 'Confirmation de sauvegarde',
icon: 'pi pi-save',
acceptLabel: 'Lancer',
rejectLabel: 'Annuler',
accept: () => {
performBackup();
}
});
};
const performBackup = () => {
setBackupInProgress(true);
setProgress(0);
const interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setBackupInProgress(false);
const newBackup: Backup = {
id: Date.now().toString(),
nom: `Sauvegarde manuelle ${new Date().toLocaleString()}`,
type: 'MANUEL',
statut: 'COMPLETE',
dateCreation: new Date(),
taille: Math.floor(Math.random() * 2000000000) + 500000000,
duree: Math.floor(Math.random() * 300) + 60,
fichiers: Math.floor(Math.random() * 10000) + 1000,
destination: 'local',
chiffre: true,
compresse: true,
utilisateur: 'admin'
};
setBackups([newBackup, ...backups]);
toast.current?.show({
severity: 'success',
summary: 'Sauvegarde terminée',
detail: 'La sauvegarde a été effectuée avec succès',
life: 5000
});
return 100;
}
return prev + 5;
});
}, 200);
};
const deleteBackup = (backup: Backup) => {
confirmDialog({
message: `Êtes-vous sûr de vouloir supprimer la sauvegarde "${backup.nom}" ?`,
header: 'Confirmation de suppression',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Supprimer',
rejectLabel: 'Annuler',
acceptClassName: 'p-button-danger',
accept: () => {
setBackups(backups.filter(b => b.id !== backup.id));
toast.current?.show({
severity: 'success',
summary: 'Suppression réussie',
detail: 'La sauvegarde a été supprimée',
life: 3000
});
}
});
};
const restoreBackup = (backup: Backup) => {
setSelectedBackup(backup);
setRestoreDialog(true);
};
const performRestore = () => {
setRestoreDialog(false);
toast.current?.show({
severity: 'info',
summary: 'Restauration en cours',
detail: 'La restauration a été lancée en arrière-plan',
life: 5000
});
};
const saveSchedule = () => {
if (newSchedule.nom) {
const schedule = {
...newSchedule,
id: Date.now().toString()
};
setSchedules([...schedules, schedule]);
setScheduleDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Planification créée',
detail: 'La planification de sauvegarde a été créée',
life: 3000
});
}
};
const toggleSchedule = (schedule: BackupSchedule) => {
const updated = schedules.map(s =>
s.id === schedule.id ? { ...s, actif: !s.actif } : s
);
setSchedules(updated);
toast.current?.show({
severity: schedule.actif ? 'warn' : 'success',
summary: schedule.actif ? 'Planification désactivée' : 'Planification activée',
detail: `La planification "${schedule.nom}" a été ${schedule.actif ? 'désactivée' : 'activée'}`,
life: 3000
});
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvelle sauvegarde"
icon="pi pi-save"
severity="success"
onClick={startManualBackup}
disabled={backupInProgress}
/>
<Button
label="Nouvelle planification"
icon="pi pi-calendar-plus"
onClick={() => {
setNewSchedule({
id: '',
nom: '',
actif: true,
frequence: 'QUOTIDIEN',
heure: new Date(),
retention: 30,
destination: 'local',
chiffrement: true,
compression: true,
inclureBase: true,
inclureFichiers: true,
inclureConfig: true
});
setScheduleDialog(true);
}}
/>
<Button
label="Importer"
icon="pi pi-upload"
severity="info"
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nettoyer"
icon="pi pi-trash"
severity="warning"
tooltip="Supprimer les anciennes sauvegardes"
onClick={() => {
confirmDialog({
message: 'Supprimer toutes les sauvegardes de plus de 90 jours ?',
header: 'Nettoyage des sauvegardes',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Nettoyer',
rejectLabel: 'Annuler',
acceptClassName: 'p-button-warning',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Nettoyage effectué',
detail: '3 anciennes sauvegardes supprimées',
life: 3000
});
}
});
}}
/>
<Button
label="Actualiser"
icon="pi pi-refresh"
onClick={loadBackupData}
/>
</div>
);
};
const statusBodyTemplate = (rowData: Backup) => {
const getSeverity = (status: string) => {
switch (status) {
case 'COMPLETE': return 'success';
case 'EN_COURS': return 'info';
case 'PARTIEL': return 'warning';
case 'ECHOUE': return 'danger';
default: return 'secondary';
}
};
const getLabel = (status: string) => {
switch (status) {
case 'COMPLETE': return 'Complète';
case 'EN_COURS': return 'En cours';
case 'PARTIEL': return 'Partielle';
case 'ECHOUE': return 'Échouée';
default: return status;
}
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const typeBodyTemplate = (rowData: Backup) => {
const getIcon = (type: string) => {
switch (type) {
case 'MANUEL': return 'pi-user';
case 'AUTOMATIQUE': return 'pi-clock';
case 'PLANIFIE': return 'pi-calendar';
default: return 'pi-save';
}
};
const getLabel = (type: string) => {
switch (type) {
case 'MANUEL': return 'Manuelle';
case 'AUTOMATIQUE': return 'Automatique';
case 'PLANIFIE': return 'Planifiée';
default: return type;
}
};
return (
<div className="flex align-items-center">
<i className={`pi ${getIcon(rowData.type)} mr-2`} />
{getLabel(rowData.type)}
</div>
);
};
const actionBodyTemplate = (rowData: Backup) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-download"
rounded
severity="info"
size="small"
tooltip="Télécharger"
/>
<Button
icon="pi pi-replay"
rounded
severity="success"
size="small"
tooltip="Restaurer"
onClick={() => restoreBackup(rowData)}
/>
<Button
icon="pi pi-trash"
rounded
severity="danger"
size="small"
tooltip="Supprimer"
onClick={() => deleteBackup(rowData)}
/>
</div>
);
};
const scheduleActionTemplate = (rowData: BackupSchedule) => {
return (
<div className="flex gap-2">
<InputSwitch
checked={rowData.actif}
onChange={() => toggleSchedule(rowData)}
/>
<Button
icon="pi pi-pencil"
rounded
severity="secondary"
size="small"
tooltip="Modifier"
/>
<Button
icon="pi pi-trash"
rounded
severity="danger"
size="small"
tooltip="Supprimer"
/>
</div>
);
};
const renderBackupList = () => (
<Card>
{backupInProgress && (
<div className="mb-4">
<h6>Sauvegarde en cours...</h6>
<ProgressBar value={progress} />
</div>
)}
<DataTable
value={backups}
paginator
rows={10}
dataKey="id"
loading={loading}
globalFilter={globalFilter}
emptyMessage="Aucune sauvegarde trouvée"
header={
<div className="flex justify-content-between align-items-center">
<h5 className="m-0">Historique des sauvegardes</h5>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
}
>
<Column field="nom" header="Nom" sortable />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
<Column
field="dateCreation"
header="Date"
body={(rowData) => formatDateTime(rowData.dateCreation)}
sortable
/>
<Column
field="taille"
header="Taille"
body={(rowData) => formatFileSize(rowData.taille)}
sortable
/>
<Column
field="duree"
header="Durée"
body={(rowData) => formatDuration(rowData.duree)}
/>
<Column field="destination" header="Destination" />
<Column
field="chiffre"
header="Sécurité"
body={(rowData) => (
<div className="flex gap-1">
{rowData.chiffre && <Tag value="Chiffré" severity="success" />}
{rowData.compresse && <Tag value="Compressé" severity="info" />}
</div>
)}
/>
<Column body={actionBodyTemplate} headerStyle={{ width: '10rem' }} />
</DataTable>
</Card>
);
const renderSchedules = () => (
<Card>
<DataTable
value={schedules}
dataKey="id"
emptyMessage="Aucune planification trouvée"
header={<h5>Planifications de sauvegarde</h5>}
>
<Column field="nom" header="Nom" />
<Column
field="frequence"
header="Fréquence"
body={(rowData) => {
const freq = frequenceOptions.find(f => f.value === rowData.frequence);
return freq ? freq.label : rowData.frequence;
}}
/>
<Column
field="heure"
header="Heure"
body={(rowData) => rowData.heure.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
/>
<Column field="destination" header="Destination" />
<Column
field="retention"
header="Rétention"
body={(rowData) => `${rowData.retention} jours`}
/>
<Column
field="actif"
header="Statut"
body={(rowData) => (
<Tag
value={rowData.actif ? 'Active' : 'Inactive'}
severity={rowData.actif ? 'success' : 'secondary'}
/>
)}
/>
<Column body={scheduleActionTemplate} headerStyle={{ width: '12rem' }} />
</DataTable>
</Card>
);
const renderStatistics = () => (
<div className="grid">
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-database text-4xl text-primary mb-3" />
<div className="text-3xl font-bold text-primary">
{formatFileSize(backups.reduce((sum, b) => sum + b.taille, 0))}
</div>
<div className="text-color-secondary">Espace total utilisé</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-save text-4xl text-green-500 mb-3" />
<div className="text-3xl font-bold text-green-500">
{backups.length}
</div>
<div className="text-color-secondary">Sauvegardes totales</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-check-circle text-4xl text-blue-500 mb-3" />
<div className="text-3xl font-bold text-blue-500">
{backups.filter(b => b.statut === 'COMPLETE').length}
</div>
<div className="text-color-secondary">Sauvegardes réussies</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<i className="pi pi-calendar text-4xl text-purple-500 mb-3" />
<div className="text-3xl font-bold text-purple-500">
{schedules.filter(s => s.actif).length}
</div>
<div className="text-color-secondary">Planifications actives</div>
</div>
</Card>
</div>
</div>
);
const scheduleDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" outlined onClick={() => setScheduleDialog(false)} />
<Button label="Créer" icon="pi pi-check" onClick={saveSchedule} />
</div>
);
const restoreDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" outlined onClick={() => setRestoreDialog(false)} />
<Button label="Restaurer" icon="pi pi-replay" severity="success" onClick={performRestore} />
</div>
);
return (
<div className="grid">
<div className="col-12">
<Card title="Gestion des sauvegardes">
<Toast ref={toast} />
<ConfirmDialog />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{renderStatistics()}
<div className="mt-4">
{renderBackupList()}
</div>
<div className="mt-4">
{renderSchedules()}
</div>
{/* Dialog pour nouvelle planification */}
<Dialog
visible={scheduleDialog}
style={{ width: '600px' }}
header="Nouvelle planification de sauvegarde"
modal
footer={scheduleDialogFooter}
onHide={() => setScheduleDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="nom">Nom de la planification</label>
<InputText
id="nom"
value={newSchedule.nom}
onChange={(e) => setNewSchedule({...newSchedule, nom: e.target.value})}
className="w-full"
placeholder="Ex: Sauvegarde quotidienne"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="frequence">Fréquence</label>
<Dropdown
id="frequence"
value={newSchedule.frequence}
options={frequenceOptions}
onChange={(e) => setNewSchedule({...newSchedule, frequence: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="heure">Heure d'exécution</label>
<Calendar
id="heure"
value={newSchedule.heure}
onChange={(e) => setNewSchedule({...newSchedule, heure: e.value || new Date()})}
timeOnly
hourFormat="24"
className="w-full"
/>
</div>
{newSchedule.frequence === 'HEBDOMADAIRE' && (
<div className="field col-12">
<label>Jours de la semaine</label>
<div className="flex flex-wrap gap-3">
{joursSemaine.map(jour => (
<div key={jour.value} className="flex align-items-center">
<Checkbox
inputId={`jour-${jour.value}`}
value={jour.value}
onChange={(e) => {
const jours = newSchedule.joursSelection || [];
if (e.checked) {
setNewSchedule({...newSchedule, joursSelection: [...jours, jour.value]});
} else {
setNewSchedule({...newSchedule, joursSelection: jours.filter(j => j !== jour.value)});
}
}}
checked={newSchedule.joursSelection?.includes(jour.value) || false}
/>
<label htmlFor={`jour-${jour.value}`} className="ml-2">{jour.label}</label>
</div>
))}
</div>
</div>
)}
<div className="field col-12 md:col-6">
<label htmlFor="destination">Destination</label>
<Dropdown
id="destination"
value={newSchedule.destination}
options={destinationOptions}
onChange={(e) => setNewSchedule({...newSchedule, destination: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="retention">Rétention (jours)</label>
<InputNumber
id="retention"
value={newSchedule.retention}
onValueChange={(e) => setNewSchedule({...newSchedule, retention: e.value || 30})}
min={1}
max={365}
className="w-full"
/>
</div>
<Divider />
<div className="field col-12">
<h6>Options de sauvegarde</h6>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="chiffrement"
checked={newSchedule.chiffrement}
onChange={(e) => setNewSchedule({...newSchedule, chiffrement: e.checked || false})}
/>
<label htmlFor="chiffrement" className="ml-2">Chiffrement</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="compression"
checked={newSchedule.compression}
onChange={(e) => setNewSchedule({...newSchedule, compression: e.checked || false})}
/>
<label htmlFor="compression" className="ml-2">Compression</label>
</div>
</div>
<div className="field col-12">
<h6>Éléments à sauvegarder</h6>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureBase"
checked={newSchedule.inclureBase}
onChange={(e) => setNewSchedule({...newSchedule, inclureBase: e.checked || false})}
/>
<label htmlFor="inclureBase" className="ml-2">Base de données</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureFichiers"
checked={newSchedule.inclureFichiers}
onChange={(e) => setNewSchedule({...newSchedule, inclureFichiers: e.checked || false})}
/>
<label htmlFor="inclureFichiers" className="ml-2">Fichiers</label>
</div>
</div>
<div className="field col-12 md:col-4">
<div className="flex align-items-center">
<Checkbox
inputId="inclureConfig"
checked={newSchedule.inclureConfig}
onChange={(e) => setNewSchedule({...newSchedule, inclureConfig: e.checked || false})}
/>
<label htmlFor="inclureConfig" className="ml-2">Configuration</label>
</div>
</div>
</div>
</Dialog>
{/* Dialog pour restauration */}
<Dialog
visible={restoreDialog}
style={{ width: '500px' }}
header="Restaurer une sauvegarde"
modal
footer={restoreDialogFooter}
onHide={() => setRestoreDialog(false)}
>
{selectedBackup && (
<div>
<div className="mb-3">
<strong>Sauvegarde:</strong> {selectedBackup.nom}
</div>
<div className="mb-3">
<strong>Date:</strong> {formatDateTime(selectedBackup.dateCreation)}
</div>
<div className="mb-3">
<strong>Taille:</strong> {formatFileSize(selectedBackup.taille)}
</div>
<Divider />
<div className="p-message p-message-warn mb-3">
<i className="pi pi-exclamation-triangle mr-2"></i>
<span>
La restauration remplacera toutes les données actuelles.
Cette action est irréversible.
</span>
</div>
<div>
<h6>Options de restauration</h6>
<div className="field-radiobutton mb-2">
<RadioButton inputId="complete" name="restoreType" value="complete" checked />
<label htmlFor="complete" className="ml-2">Restauration complète</label>
</div>
<div className="field-radiobutton mb-2">
<RadioButton inputId="selective" name="restoreType" value="selective" />
<label htmlFor="selective" className="ml-2">Restauration sélective</label>
</div>
</div>
</div>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default SauvegardePage;

View File

@@ -0,0 +1,990 @@
'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 const }
: 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="warning"
onClick={() => resetPassword(rowData)}
tooltip="Réinitialiser mot de passe"
/>
<ActionButton
icon={rowData.statut === 'SUSPENDU' ? 'pi pi-check' : 'pi pi-ban'}
color={rowData.statut === 'SUSPENDU' ? 'info' : 'danger'}
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 = 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" severity="danger" 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)}
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;