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,843 @@
'use client';
import React, { useState, useEffect, useRef } 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 { TabView, TabPanel } from 'primereact/tabview';
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 { ToggleButton } from 'primereact/togglebutton';
import { Panel } from 'primereact/panel';
import { Divider } from 'primereact/divider';
import { Password } from 'primereact/password';
import { Checkbox } from 'primereact/checkbox';
import { Calendar } from 'primereact/calendar';
import { InputNumber } from 'primereact/inputnumber';
import { Badge } from 'primereact/badge';
import { formatDate } from '../../../utils/formatters';
interface User {
id: string;
nom: string;
prenom: string;
email: string;
telephone: string;
role: 'ADMIN' | 'USER' | 'VIEWER';
actif: boolean;
derniereConnexion: Date;
dateCreation: Date;
permissions: string[];
}
interface SystemLog {
id: string;
timestamp: Date;
niveau: 'INFO' | 'WARNING' | 'ERROR';
message: string;
utilisateur: string;
module: string;
action: string;
}
interface SystemConfig {
id: string;
cle: string;
valeur: string;
description: string;
type: 'string' | 'number' | 'boolean' | 'date';
modifiable: boolean;
categorie: string;
}
const AdministrationPage = () => {
const [users, setUsers] = useState<User[]>([]);
const [logs, setLogs] = useState<SystemLog[]>([]);
const [config, setConfig] = useState<SystemConfig[]>([]);
const [loading, setLoading] = useState(true);
const [activeIndex, setActiveIndex] = useState(0);
const [globalFilter, setGlobalFilter] = useState('');
const [userDialog, setUserDialog] = useState(false);
const [configDialog, setConfigDialog] = useState(false);
const [deleteUserDialog, setDeleteUserDialog] = useState(false);
const [user, setUser] = useState<User>({
id: '',
nom: '',
prenom: '',
email: '',
telephone: '',
role: 'USER',
actif: true,
derniereConnexion: new Date(),
dateCreation: new Date(),
permissions: []
});
const [configItem, setConfigItem] = useState<SystemConfig>({
id: '',
cle: '',
valeur: '',
description: '',
type: 'string',
modifiable: true,
categorie: 'general'
});
const [submitted, setSubmitted] = useState(false);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<User[]>>(null);
const roles = [
{ label: 'Administrateur', value: 'ADMIN' },
{ label: 'Utilisateur', value: 'USER' },
{ label: 'Lecture seule', value: 'VIEWER' }
];
const configTypes = [
{ label: 'Texte', value: 'string' },
{ label: 'Nombre', value: 'number' },
{ label: 'Booléen', value: 'boolean' },
{ label: 'Date', value: 'date' }
];
const configCategories = [
{ label: 'Général', value: 'general' },
{ label: 'Sécurité', value: 'security' },
{ label: 'Email', value: 'email' },
{ label: 'Sauvegarde', value: 'backup' },
{ label: 'Performance', value: 'performance' }
];
const availablePermissions = [
'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',
'rapports.read',
'administration.read',
'administration.write'
];
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Simulation de données
const mockUsers: User[] = [
{
id: '1',
nom: 'Admin',
prenom: 'BTP Xpress',
email: 'admin@btpxpress.com',
telephone: '06 12 34 56 78',
role: 'ADMIN',
actif: true,
derniereConnexion: new Date(),
dateCreation: new Date('2024-01-01'),
permissions: availablePermissions
},
{
id: '2',
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@btpxpress.com',
telephone: '06 98 76 54 32',
role: 'USER',
actif: true,
derniereConnexion: new Date(Date.now() - 3600000), // 1 heure
dateCreation: new Date('2024-02-01'),
permissions: ['clients.read', 'clients.write', 'chantiers.read', 'chantiers.write']
}
];
const mockLogs: SystemLog[] = [
{
id: '1',
timestamp: new Date(),
niveau: 'INFO',
message: 'Utilisateur connecté',
utilisateur: 'admin@btpxpress.com',
module: 'Auth',
action: 'LOGIN'
},
{
id: '2',
timestamp: new Date(Date.now() - 600000), // 10 minutes
niveau: 'INFO',
message: 'Nouveau client créé',
utilisateur: 'jean.dupont@btpxpress.com',
module: 'Clients',
action: 'CREATE'
},
{
id: '3',
timestamp: new Date(Date.now() - 1800000), // 30 minutes
niveau: 'WARNING',
message: 'Tentative de connexion échouée',
utilisateur: 'unknown@email.com',
module: 'Auth',
action: 'LOGIN_FAILED'
}
];
const mockConfig: SystemConfig[] = [
{
id: '1',
cle: 'app.name',
valeur: 'BTP Xpress',
description: 'Nom de l\'application',
type: 'string',
modifiable: true,
categorie: 'general'
},
{
id: '2',
cle: 'app.version',
valeur: '1.0.0',
description: 'Version de l\'application',
type: 'string',
modifiable: false,
categorie: 'general'
},
{
id: '3',
cle: 'auth.session_timeout',
valeur: '3600',
description: 'Durée de session en secondes',
type: 'number',
modifiable: true,
categorie: 'security'
},
{
id: '4',
cle: 'email.smtp_enabled',
valeur: 'true',
description: 'Activation du serveur SMTP',
type: 'boolean',
modifiable: true,
categorie: 'email'
}
];
setUsers(mockUsers);
setLogs(mockLogs);
setConfig(mockConfig);
} 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 openNewUser = () => {
setUser({
id: '',
nom: '',
prenom: '',
email: '',
telephone: '',
role: 'USER',
actif: true,
derniereConnexion: new Date(),
dateCreation: new Date(),
permissions: []
});
setPassword('');
setConfirmPassword('');
setSubmitted(false);
setUserDialog(true);
};
const editUser = (user: User) => {
setUser({ ...user });
setPassword('');
setConfirmPassword('');
setUserDialog(true);
};
const confirmDeleteUser = (user: User) => {
setUser(user);
setDeleteUserDialog(true);
};
const deleteUser = () => {
const updatedUsers = users.filter(u => u.id !== user.id);
setUsers(updatedUsers);
setDeleteUserDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur supprimé',
life: 3000
});
};
const saveUser = () => {
setSubmitted(true);
if (user.nom.trim() && user.prenom.trim() && user.email.trim()) {
if (!user.id && (!password || password !== confirmPassword)) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Mot de passe requis et doit correspondre',
life: 3000
});
return;
}
let updatedUsers = [...users];
if (user.id) {
// Mise à jour
const index = users.findIndex(u => u.id === user.id);
updatedUsers[index] = user;
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur mis à jour',
life: 3000
});
} else {
// Création
const newUser = {
...user,
id: Date.now().toString(),
dateCreation: new Date()
};
updatedUsers.push(newUser);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Utilisateur créé',
life: 3000
});
}
setUsers(updatedUsers);
setUserDialog(false);
}
};
const editConfig = (config: SystemConfig) => {
setConfigItem({ ...config });
setConfigDialog(true);
};
const saveConfig = () => {
const updatedConfig = config.map(c =>
c.id === configItem.id ? configItem : c
);
setConfig(updatedConfig);
setConfigDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Configuration mise à jour',
life: 3000
});
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
const val = (e.target && e.target.value) || '';
let _user = { ...user };
(_user as any)[name] = val;
setUser(_user);
};
const onDropdownChange = (e: any, name: string) => {
let _user = { ...user };
(_user as any)[name] = e.value;
setUser(_user);
};
const onPermissionChange = (e: any, permission: string) => {
let _user = { ...user };
if (e.checked) {
_user.permissions = [..._user.permissions, permission];
} else {
_user.permissions = _user.permissions.filter(p => p !== permission);
}
setUser(_user);
};
const onConfigInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
const val = (e.target && e.target.value) || '';
let _config = { ...configItem };
(_config as any)[name] = val;
setConfigItem(_config);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvel utilisateur"
icon="pi pi-plus"
severity="success"
onClick={openNewUser}
/>
<Button
label="Actualiser"
icon="pi pi-refresh"
onClick={loadData}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
);
};
const actionBodyTemplate = (rowData: User) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
rounded
severity="success"
onClick={() => editUser(rowData)}
/>
<Button
icon="pi pi-trash"
rounded
severity="warning"
onClick={() => confirmDeleteUser(rowData)}
/>
</div>
);
};
const statusBodyTemplate = (rowData: User) => {
return (
<Tag
value={rowData.actif ? 'Actif' : 'Inactif'}
severity={rowData.actif ? 'success' : 'danger'}
/>
);
};
const roleBodyTemplate = (rowData: User) => {
const roleLabels = {
'ADMIN': 'Administrateur',
'USER': 'Utilisateur',
'VIEWER': 'Lecture seule'
};
return roleLabels[rowData.role] || rowData.role;
};
const logLevelBodyTemplate = (rowData: SystemLog) => {
const getSeverity = (level: string) => {
switch (level) {
case 'INFO': return 'info';
case 'WARNING': return 'warning';
case 'ERROR': return 'danger';
default: return 'info';
}
};
return (
<Tag
value={rowData.niveau}
severity={getSeverity(rowData.niveau)}
/>
);
};
const configActionBodyTemplate = (rowData: SystemConfig) => {
return (
<Button
icon="pi pi-pencil"
rounded
severity="success"
onClick={() => editConfig(rowData)}
disabled={!rowData.modifiable}
/>
);
};
const configValueBodyTemplate = (rowData: SystemConfig) => {
if (rowData.type === 'boolean') {
return (
<Tag
value={rowData.valeur === 'true' ? 'Activé' : 'Désactivé'}
severity={rowData.valeur === 'true' ? 'success' : 'danger'}
/>
);
}
return rowData.valeur;
};
const renderUsersTab = () => {
return (
<div className="grid">
<div className="col-12">
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={users}
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é."
loading={loading}
>
<Column field="prenom" header="Prénom" sortable />
<Column field="nom" header="Nom" sortable />
<Column field="email" header="Email" sortable />
<Column field="role" header="Rôle" body={roleBodyTemplate} sortable />
<Column field="actif" header="Statut" body={statusBodyTemplate} />
<Column field="derniereConnexion" header="Dernière connexion" body={(rowData) => formatDate(rowData.derniereConnexion)} sortable />
<Column body={actionBodyTemplate} />
</DataTable>
</div>
</div>
);
};
const renderLogsTab = () => {
return (
<div className="grid">
<div className="col-12">
<DataTable
value={logs}
dataKey="id"
paginator
rows={15}
className="datatable-responsive"
globalFilter={globalFilter}
emptyMessage="Aucun log trouvé."
loading={loading}
>
<Column field="timestamp" header="Date/Heure" body={(rowData) => formatDate(rowData.timestamp)} sortable />
<Column field="niveau" header="Niveau" body={logLevelBodyTemplate} sortable />
<Column field="module" header="Module" sortable />
<Column field="action" header="Action" sortable />
<Column field="utilisateur" header="Utilisateur" sortable />
<Column field="message" header="Message" />
</DataTable>
</div>
</div>
);
};
const renderConfigTab = () => {
return (
<div className="grid">
<div className="col-12">
<DataTable
value={config}
dataKey="id"
paginator
rows={15}
className="datatable-responsive"
globalFilter={globalFilter}
emptyMessage="Aucune configuration trouvée."
loading={loading}
>
<Column field="cle" header="Clé" sortable />
<Column field="valeur" header="Valeur" body={configValueBodyTemplate} />
<Column field="description" header="Description" />
<Column field="categorie" header="Catégorie" sortable />
<Column field="type" header="Type" sortable />
<Column body={configActionBodyTemplate} />
</DataTable>
</div>
</div>
);
};
const userDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setUserDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveUser} />
</div>
);
const configDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setConfigDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveConfig} />
</div>
);
const deleteUserDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Non" icon="pi pi-times" text onClick={() => setDeleteUserDialog(false)} />
<Button label="Oui" icon="pi pi-check" onClick={deleteUser} />
</div>
);
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">
{renderUsersTab()}
</TabPanel>
<TabPanel header="Logs Système" leftIcon="pi pi-list mr-2">
{renderLogsTab()}
</TabPanel>
<TabPanel header="Configuration" leftIcon="pi pi-cog mr-2">
{renderConfigTab()}
</TabPanel>
</TabView>
{/* Dialog utilisateur */}
<Dialog
visible={userDialog}
style={{ width: '700px' }}
header="Détails de l'utilisateur"
modal
className="p-fluid"
footer={userDialogFooter}
onHide={() => setUserDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="prenom">Prénom</label>
<InputText
id="prenom"
value={user.prenom}
onChange={(e) => onInputChange(e, 'prenom')}
required
className={submitted && !user.prenom ? 'p-invalid' : ''}
/>
{submitted && !user.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={user.nom}
onChange={(e) => onInputChange(e, 'nom')}
required
className={submitted && !user.nom ? 'p-invalid' : ''}
/>
{submitted && !user.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={user.email}
onChange={(e) => onInputChange(e, 'email')}
required
className={submitted && !user.email ? 'p-invalid' : ''}
/>
{submitted && !user.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={user.telephone}
onChange={(e) => onInputChange(e, 'telephone')}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="role">Rôle</label>
<Dropdown
id="role"
value={user.role}
options={roles}
onChange={(e) => onDropdownChange(e, 'role')}
placeholder="Sélectionnez un rôle"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="actif">Statut</label>
<div className="flex align-items-center">
<ToggleButton
checked={user.actif}
onChange={(e) => setUser({ ...user, actif: e.value })}
onLabel="Actif"
offLabel="Inactif"
/>
</div>
</div>
{!user.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 className="field col-12">
<label>Permissions</label>
<div className="grid">
{availablePermissions.map(permission => (
<div key={permission} className="col-12 md:col-6">
<div className="flex align-items-center">
<Checkbox
checked={user.permissions.includes(permission)}
onChange={(e) => onPermissionChange(e, permission)}
/>
<label className="ml-2">{permission}</label>
</div>
</div>
))}
</div>
</div>
</div>
</Dialog>
{/* Dialog configuration */}
<Dialog
visible={configDialog}
style={{ width: '500px' }}
header="Configuration"
modal
className="p-fluid"
footer={configDialogFooter}
onHide={() => setConfigDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="cle">Clé</label>
<InputText
id="cle"
value={configItem.cle}
disabled
/>
</div>
<div className="field col-12">
<label htmlFor="valeur">Valeur</label>
{configItem.type === 'boolean' ? (
<ToggleButton
checked={configItem.valeur === 'true'}
onChange={(e) => setConfigItem({ ...configItem, valeur: e.value ? 'true' : 'false' })}
onLabel="Activé"
offLabel="Désactivé"
/>
) : configItem.type === 'number' ? (
<InputNumber
value={parseInt(configItem.valeur)}
onValueChange={(e) => setConfigItem({ ...configItem, valeur: e.value?.toString() || '' })}
/>
) : (
<InputText
id="valeur"
value={configItem.valeur}
onChange={(e) => onConfigInputChange(e, 'valeur')}
/>
)}
</div>
<div className="field col-12">
<label htmlFor="description">Description</label>
<InputTextarea
id="description"
value={configItem.description}
disabled
rows={3}
/>
</div>
</div>
</Dialog>
{/* Dialog suppression utilisateur */}
<Dialog
visible={deleteUserDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={deleteUserDialogFooter}
onHide={() => setDeleteUserDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{user && (
<span>
Êtes-vous sûr de vouloir supprimer l'utilisateur <b>{user.prenom} {user.nom}</b> ?
</span>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default AdministrationPage;