Compare commits
5 Commits
139a2357f4
...
62e7e32fac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e7e32fac | ||
| 5ed9a80339 | |||
|
|
ec5c54e4c3 | ||
|
|
96eb956b0b | ||
|
|
6ebca4945c |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
@@ -710,3 +711,717 @@ const ProtectedRolesPermissionsPage = () => {
|
||||
};
|
||||
|
||||
export default ProtectedRolesPermissionsPage;
|
||||
=======
|
||||
'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)}
|
||||
selectionMode="multiple"
|
||||
dataKey="id"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
className="datatable-responsive"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} 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;
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -560,4 +560,8 @@ const ExecutionGranulaireChantier = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default ExecutionGranulaireChantier;
|
||||
=======
|
||||
export default ExecutionGranulaireChantier;
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
@@ -327,3 +328,334 @@ const PhasesCleanPage: React.FC = () => {
|
||||
export default PhasesCleanPage;
|
||||
|
||||
|
||||
=======
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
|
||||
import PhasesTable from '../../../../../components/phases/PhasesTable';
|
||||
import usePhasesManager from '../../../../../hooks/usePhasesManager';
|
||||
import type { PhaseChantier, PhaseFormData } from '../../../../../types/btp-extended';
|
||||
|
||||
const PhasesCleanPage: React.FC = () => {
|
||||
const params = useParams();
|
||||
const chantierId = params?.id as string;
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
// Hook de gestion des phases
|
||||
const {
|
||||
phases,
|
||||
loading,
|
||||
selectedPhase,
|
||||
setSelectedPhase,
|
||||
loadPhases,
|
||||
createPhase,
|
||||
updatePhase,
|
||||
deletePhase,
|
||||
startPhase,
|
||||
setToastRef
|
||||
} = usePhasesManager({ chantierId });
|
||||
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
|
||||
// États pour les dialogues
|
||||
const [showPhaseDialog, setShowPhaseDialog] = useState(false);
|
||||
const [editingPhase, setEditingPhase] = useState(false);
|
||||
|
||||
// États pour les formulaires
|
||||
const [phaseForm, setPhaseForm] = useState<PhaseFormData>({
|
||||
nom: '',
|
||||
description: '',
|
||||
dateDebutPrevue: '',
|
||||
dateFinPrevue: '',
|
||||
dureeEstimeeHeures: 8,
|
||||
priorite: 'MOYENNE',
|
||||
critique: false,
|
||||
statut: 'PLANIFIEE',
|
||||
ordreExecution: 1,
|
||||
budgetPrevu: 0,
|
||||
coutReel: 0,
|
||||
prerequisPhases: [],
|
||||
competencesRequises: [],
|
||||
materielsNecessaires: [],
|
||||
fournisseursRecommandes: []
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
useEffect(() => {
|
||||
setToastRef(toast.current);
|
||||
if (chantierId) {
|
||||
loadPhases();
|
||||
}
|
||||
}, [chantierId, setToastRef, loadPhases]);
|
||||
|
||||
// Gestionnaires d'événements
|
||||
const handleEditPhase = (phase: PhaseChantier) => {
|
||||
setSelectedPhase(phase);
|
||||
setEditingPhase(true);
|
||||
setPhaseForm({
|
||||
nom: phase.nom,
|
||||
description: phase.description || '',
|
||||
dateDebutPrevue: phase.dateDebutPrevue || '',
|
||||
dateFinPrevue: phase.dateFinPrevue || '',
|
||||
dureeEstimeeHeures: phase.dureeEstimeeHeures || 8,
|
||||
priorite: phase.priorite || 'MOYENNE',
|
||||
critique: phase.critique || false,
|
||||
statut: phase.statut,
|
||||
ordreExecution: phase.ordreExecution || 1,
|
||||
budgetPrevu: phase.budgetPrevu || 0,
|
||||
coutReel: phase.coutReel || 0,
|
||||
prerequisPhases: phase.prerequisPhases || [],
|
||||
competencesRequises: phase.competencesRequises || [],
|
||||
materielsNecessaires: phase.materielsNecessaires || [],
|
||||
fournisseursRecommandes: phase.fournisseursRecommandes || []
|
||||
});
|
||||
setShowPhaseDialog(true);
|
||||
};
|
||||
|
||||
const handleSavePhase = async () => {
|
||||
if (!phaseForm.nom.trim()) {
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Données manquantes',
|
||||
detail: 'Veuillez remplir le nom de la phase',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const phaseData = {
|
||||
...phaseForm,
|
||||
chantierId: chantierId
|
||||
};
|
||||
|
||||
if (editingPhase && selectedPhase) {
|
||||
await updatePhase(selectedPhase.id!, phaseData);
|
||||
} else {
|
||||
await createPhase(phaseData);
|
||||
}
|
||||
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
setSelectedPhase(null);
|
||||
resetPhaseForm();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde de la phase:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetPhaseForm = () => {
|
||||
setPhaseForm({
|
||||
nom: '',
|
||||
description: '',
|
||||
dateDebutPrevue: '',
|
||||
dateFinPrevue: '',
|
||||
dureeEstimeeHeures: 8,
|
||||
priorite: 'MOYENNE',
|
||||
critique: false,
|
||||
statut: 'PLANIFIEE',
|
||||
ordreExecution: 1,
|
||||
budgetPrevu: 0,
|
||||
coutReel: 0,
|
||||
prerequisPhases: [],
|
||||
competencesRequises: [],
|
||||
materielsNecessaires: [],
|
||||
fournisseursRecommandes: []
|
||||
});
|
||||
};
|
||||
|
||||
// Templates de la toolbar
|
||||
const toolbarStartTemplate = (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<h5 className="m-0 text-color">Phases - Version Clean</h5>
|
||||
<Badge value={phases.length} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const toolbarEndTemplate = (
|
||||
<div className="flex gap-2">
|
||||
<span className="p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
placeholder="Rechercher..."
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
label="Nouvelle phase"
|
||||
icon="pi pi-plus"
|
||||
className="p-button-success"
|
||||
onClick={() => {
|
||||
resetPhaseForm();
|
||||
setEditingPhase(false);
|
||||
setShowPhaseDialog(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toolbar
|
||||
start={toolbarStartTemplate}
|
||||
end={toolbarEndTemplate}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<PhasesTable
|
||||
phases={phases}
|
||||
loading={loading}
|
||||
chantierId={chantierId}
|
||||
showStats={false}
|
||||
showChantierColumn={false}
|
||||
showSubPhases={true}
|
||||
showBudget={true}
|
||||
showExpansion={true}
|
||||
actions={['view', 'edit', 'delete', 'start']}
|
||||
onRefresh={loadPhases}
|
||||
onPhaseEdit={handleEditPhase}
|
||||
onPhaseDelete={deletePhase}
|
||||
onPhaseStart={startPhase}
|
||||
rows={15}
|
||||
globalFilter={globalFilter}
|
||||
showGlobalFilter={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Dialog pour créer/modifier une phase */}
|
||||
<Dialog
|
||||
header={editingPhase ? "Modifier la phase" : "Nouvelle phase"}
|
||||
visible={showPhaseDialog}
|
||||
onHide={() => {
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
resetPhaseForm();
|
||||
}}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-text"
|
||||
onClick={() => {
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
resetPhaseForm();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label={editingPhase ? "Modifier" : "Créer"}
|
||||
icon="pi pi-check"
|
||||
className="p-button-success"
|
||||
onClick={handleSavePhase}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{ width: '50vw', maxWidth: '600px' }}
|
||||
modal
|
||||
>
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseNom" className="font-semibold">
|
||||
Nom de la phase <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="phaseNom"
|
||||
value={phaseForm.nom}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, nom: e.target.value }))}
|
||||
className="w-full"
|
||||
placeholder="Ex: Fondations, Gros œuvre..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseDescription" className="font-semibold">Description</label>
|
||||
<InputTextarea
|
||||
id="phaseDescription"
|
||||
value={phaseForm.description}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full"
|
||||
rows={3}
|
||||
placeholder="Description détaillée de la phase..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseDuree" className="font-semibold">Durée estimée (heures)</label>
|
||||
<InputNumber
|
||||
id="phaseDuree"
|
||||
value={phaseForm.dureeEstimeeHeures}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, dureeEstimeeHeures: e.value || 0 }))}
|
||||
className="w-full"
|
||||
min={1}
|
||||
suffix=" h"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="phasePriorite" className="font-semibold">Priorité</label>
|
||||
<Dropdown
|
||||
id="phasePriorite"
|
||||
value={phaseForm.priorite}
|
||||
options={[
|
||||
{ label: 'Faible', value: 'FAIBLE' },
|
||||
{ label: 'Moyenne', value: 'MOYENNE' },
|
||||
{ label: 'Élevée', value: 'ELEVEE' },
|
||||
{ label: 'Critique', value: 'CRITIQUE' }
|
||||
]}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, priorite: e.value }))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field-checkbox">
|
||||
<Checkbox
|
||||
id="phaseCritique"
|
||||
checked={phaseForm.critique}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, critique: e.checked }))}
|
||||
/>
|
||||
<label htmlFor="phaseCritique" className="ml-2 font-semibold">
|
||||
Phase critique
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhasesCleanPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
@@ -339,3 +340,346 @@ export default ChantiersEnCoursPage;
|
||||
|
||||
|
||||
|
||||
=======
|
||||
'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 { InputText } from 'primereact/inputtext';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { chantierService } from '../../../../services/api';
|
||||
import { formatDate, formatCurrency } from '../../../../utils/formatters';
|
||||
import type { Chantier, StatutChantier } from '../../../../types/btp';
|
||||
|
||||
const ChantiersEnCoursPage = () => {
|
||||
const [chantiers, setChantiers] = useState<Chantier[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [selectedChantiers, setSelectedChantiers] = useState<Chantier[]>([]);
|
||||
const [updateDialog, setUpdateDialog] = useState(false);
|
||||
const [selectedChantier, setSelectedChantier] = useState<Chantier | null>(null);
|
||||
const [updateData, setUpdateData] = useState({ dateFinReelle: null, montantReel: 0 });
|
||||
const toast = useRef<Toast>(null);
|
||||
const dt = useRef<DataTable<Chantier[]>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadChantiers();
|
||||
}, []);
|
||||
|
||||
const loadChantiers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await chantierService.getAll();
|
||||
// Filtrer seulement les chantiers en cours
|
||||
const chantiersEnCours = data.filter(chantier => chantier.statut === 'EN_COURS');
|
||||
setChantiers(chantiersEnCours);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des chantiers:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les chantiers en cours',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateProgress = (chantier: Chantier) => {
|
||||
if (!chantier.dateDebut || !chantier.dateFinPrevue) return 0;
|
||||
|
||||
const now = new Date();
|
||||
const start = new Date(chantier.dateDebut);
|
||||
const end = new Date(chantier.dateFinPrevue);
|
||||
const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const elapsedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
return Math.min(Math.max((elapsedDays / totalDays) * 100, 0), 100);
|
||||
};
|
||||
|
||||
const isLate = (chantier: Chantier) => {
|
||||
if (!chantier.dateFinPrevue) return false;
|
||||
const today = new Date();
|
||||
const endDate = new Date(chantier.dateFinPrevue);
|
||||
return today > endDate;
|
||||
};
|
||||
|
||||
const markAsCompleted = (chantier: Chantier) => {
|
||||
setSelectedChantier(chantier);
|
||||
setUpdateData({
|
||||
dateFinReelle: new Date(),
|
||||
montantReel: chantier.montantReel || chantier.montantPrevu || 0
|
||||
});
|
||||
setUpdateDialog(true);
|
||||
};
|
||||
|
||||
const handleCompleteChantier = async () => {
|
||||
if (!selectedChantier) return;
|
||||
|
||||
try {
|
||||
const updatedChantier = {
|
||||
...selectedChantier,
|
||||
statut: 'TERMINE' as StatutChantier,
|
||||
dateFinReelle: updateData.dateFinReelle,
|
||||
montantReel: updateData.montantReel
|
||||
};
|
||||
|
||||
await chantierService.update(selectedChantier.id, updatedChantier);
|
||||
|
||||
// Retirer le chantier de la liste car il n'est plus "en cours"
|
||||
setChantiers(prev => prev.filter(c => c.id !== selectedChantier.id));
|
||||
setUpdateDialog(false);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Succès',
|
||||
detail: 'Chantier marqué comme terminé',
|
||||
life: 3000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de mettre à jour le chantier',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
dt.current?.exportCSV();
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="my-2">
|
||||
<h5 className="m-0">Chantiers en cours ({chantiers.length})</h5>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<Button
|
||||
label="Exporter"
|
||||
icon="pi pi-upload"
|
||||
severity="help"
|
||||
onClick={exportCSV}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: Chantier) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-check"
|
||||
rounded
|
||||
severity="success"
|
||||
size="small"
|
||||
tooltip="Marquer comme terminé"
|
||||
onClick={() => markAsCompleted(rowData)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
rounded
|
||||
severity="info"
|
||||
size="small"
|
||||
tooltip="Voir détails"
|
||||
onClick={() => {
|
||||
toast.current?.show({
|
||||
severity: 'info',
|
||||
summary: 'Info',
|
||||
detail: `Détails du chantier ${rowData.nom}`,
|
||||
life: 3000
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const statusBodyTemplate = (rowData: Chantier) => {
|
||||
const late = isLate(rowData);
|
||||
return (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Tag value="En cours" severity="success" />
|
||||
{late && <Tag value="En retard" severity="danger" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const progressBodyTemplate = (rowData: Chantier) => {
|
||||
const progress = calculateProgress(rowData);
|
||||
const late = isLate(rowData);
|
||||
|
||||
return (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<ProgressBar
|
||||
value={progress}
|
||||
style={{ height: '6px', width: '100px' }}
|
||||
color={late ? '#ef4444' : undefined}
|
||||
/>
|
||||
<span className={`text-sm ${late ? 'text-red-500' : ''}`}>
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const clientBodyTemplate = (rowData: Chantier) => {
|
||||
if (!rowData.client) return '';
|
||||
return `${rowData.client.prenom} ${rowData.client.nom}`;
|
||||
};
|
||||
|
||||
const dateBodyTemplate = (rowData: Chantier, field: string) => {
|
||||
const date = (rowData as any)[field];
|
||||
const late = field === 'dateFinPrevue' && isLate(rowData);
|
||||
|
||||
return (
|
||||
<span className={late ? 'text-red-500 font-bold' : ''}>
|
||||
{date ? formatDate(date) : ''}
|
||||
{late && <i className="pi pi-exclamation-triangle ml-1"></i>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const budgetBodyTemplate = (rowData: Chantier) => {
|
||||
const prevu = rowData.montantPrevu || 0;
|
||||
const reel = rowData.montantReel || 0;
|
||||
const progress = calculateProgress(rowData);
|
||||
const estimated = prevu * (progress / 100);
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<div>Prévu: {formatCurrency(prevu)}</div>
|
||||
<div className="text-600">Estimé: {formatCurrency(estimated)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
||||
<h5 className="m-0">Chantiers en cours de réalisation</h5>
|
||||
<span className="block mt-2 md:mt-0 p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
type="search"
|
||||
placeholder="Rechercher..."
|
||||
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const completeDialogFooter = (
|
||||
<>
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
onClick={() => setUpdateDialog(false)}
|
||||
/>
|
||||
<Button
|
||||
label="Marquer comme terminé"
|
||||
icon="pi pi-check"
|
||||
text
|
||||
onClick={handleCompleteChantier}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toast ref={toast} />
|
||||
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
|
||||
|
||||
<DataTable
|
||||
ref={dt}
|
||||
value={chantiers}
|
||||
selection={selectedChantiers}
|
||||
onSelectionChange={(e) => setSelectedChantiers(e.value)}
|
||||
selectionMode="multiple"
|
||||
dataKey="id"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
className="datatable-responsive"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} chantiers"
|
||||
globalFilter={globalFilter}
|
||||
emptyMessage="Aucun chantier en cours trouvé."
|
||||
header={header}
|
||||
responsiveLayout="scroll"
|
||||
loading={loading}
|
||||
>
|
||||
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
|
||||
<Column field="nom" header="Nom" sortable headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column field="client" header="Client" body={clientBodyTemplate} sortable headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column field="adresse" header="Adresse" sortable headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column field="dateDebut" header="Date début" body={(rowData) => dateBodyTemplate(rowData, 'dateDebut')} sortable headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="dateFinPrevue" header="Date fin prévue" body={(rowData) => dateBodyTemplate(rowData, 'dateFinPrevue')} sortable headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="statut" header="Statut" body={statusBodyTemplate} headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="progress" header="Progression" body={progressBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column field="budget" header="Budget" body={budgetBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '8rem' }} />
|
||||
</DataTable>
|
||||
|
||||
<Dialog
|
||||
visible={updateDialog}
|
||||
style={{ width: '450px' }}
|
||||
header="Finaliser le chantier"
|
||||
modal
|
||||
className="p-fluid"
|
||||
footer={completeDialogFooter}
|
||||
onHide={() => setUpdateDialog(false)}
|
||||
>
|
||||
<div className="formgrid grid">
|
||||
<div className="field col-12">
|
||||
<label htmlFor="dateFinReelle">Date de fin réelle</label>
|
||||
<Calendar
|
||||
id="dateFinReelle"
|
||||
value={updateData.dateFinReelle}
|
||||
onChange={(e) => setUpdateData(prev => ({ ...prev, dateFinReelle: e.value }))}
|
||||
dateFormat="dd/mm/yy"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12">
|
||||
<label htmlFor="montantReel">Montant réel (€)</label>
|
||||
<InputNumber
|
||||
id="montantReel"
|
||||
value={updateData.montantReel}
|
||||
onValueChange={(e) => setUpdateData(prev => ({ ...prev, montantReel: e.value || 0 }))}
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="fr-FR"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChantiersEnCoursPage;
|
||||
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -374,6 +374,12 @@ const ChantiersExecutionGranulaire = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default ChantiersExecutionGranulaire;
|
||||
|
||||
|
||||
=======
|
||||
export default ChantiersExecutionGranulaire;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -375,5 +375,10 @@ const ChantiersPlanifiesPage = () => {
|
||||
};
|
||||
|
||||
export default ChantiersPlanifiesPage;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
=======
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -473,5 +473,10 @@ STATISTIQUES:
|
||||
};
|
||||
|
||||
export default ChantiersRetardPage;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
=======
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -403,5 +403,10 @@ Variance budgétaire: ${formatCurrency(totalCost - totalBudget)} (${Math.round((
|
||||
};
|
||||
|
||||
export default ChantiersTerminesPage;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
=======
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -581,7 +581,14 @@ const WorkflowChantiers = () => {
|
||||
};
|
||||
|
||||
export default WorkflowChantiers;
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
|
||||
=======
|
||||
|
||||
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -487,5 +487,10 @@ const RechercheClientPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default RechercheClientPage;
|
||||
|
||||
=======
|
||||
export default RechercheClientPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -535,4 +535,8 @@ className="w-full md:w-14rem"
|
||||
};
|
||||
|
||||
export default DashboardMaintenance;
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -718,4 +718,8 @@ const DashboardRessources = () => {
|
||||
};
|
||||
|
||||
export default DashboardRessources;
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -607,6 +607,12 @@ RECOMMANDATIONS:
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default FacturesPayeesPage;
|
||||
|
||||
|
||||
=======
|
||||
export default FacturesPayeesPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -712,6 +712,12 @@ ACTIONS RECOMMANDÉES:
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default FacturesRetardPage;
|
||||
|
||||
|
||||
=======
|
||||
export default FacturesRetardPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -310,5 +310,10 @@ const MaintenancesEnCoursPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default MaintenancesEnCoursPage;
|
||||
|
||||
=======
|
||||
export default MaintenancesEnCoursPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -445,5 +445,10 @@ const MaintenancesPlanifieesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default MaintenancesPlanifieesPage;
|
||||
|
||||
=======
|
||||
export default MaintenancesPlanifieesPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -324,5 +324,10 @@ const MaterielsDisponiblesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default MaterielsDisponiblesPage;
|
||||
|
||||
=======
|
||||
export default MaterielsDisponiblesPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -339,5 +339,10 @@ const MaintenancePrevuePage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default MaintenancePrevuePage;
|
||||
|
||||
=======
|
||||
export default MaintenancePrevuePage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -255,4 +255,8 @@ const RechercheMaterielsPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default RechercheMaterielsPage;
|
||||
=======
|
||||
export default RechercheMaterielsPage;
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -551,4 +551,8 @@ const LandingPage: Page = () => {
|
||||
};
|
||||
|
||||
export default LandingPage;
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -494,4 +494,8 @@ const PhasesEnRetardPage: Page = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default PhasesEnRetardPage;
|
||||
=======
|
||||
export default PhasesEnRetardPage;
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -1010,6 +1010,12 @@ const CalendrierPlanningPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default CalendrierPlanningPage;
|
||||
|
||||
|
||||
=======
|
||||
export default CalendrierPlanningPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -1351,6 +1351,12 @@ const EquipesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default EquipesPage;
|
||||
|
||||
|
||||
=======
|
||||
export default EquipesPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -1549,6 +1549,12 @@ const MaterielPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default MaterielPage;
|
||||
|
||||
|
||||
=======
|
||||
export default MaterielPage;
|
||||
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -836,5 +836,10 @@ const CommandesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default CommandesPage;
|
||||
|
||||
=======
|
||||
export default CommandesPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
@@ -1022,5 +1022,10 @@ const SortiesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
export default SortiesPage;
|
||||
|
||||
=======
|
||||
export default SortiesPage;
|
||||
|
||||
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1
|
||||
|
||||
21
app/clients/page.tsx
Normal file
21
app/clients/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const ClientsPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Rediriger vers la page principale des clients dans (main)
|
||||
router.replace('/(main)/clients');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Redirection vers les clients...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientsPage;
|
||||
21
app/home/page.tsx
Normal file
21
app/home/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const HomePage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Rediriger vers la page d'accueil principale
|
||||
router.replace('/');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Redirection vers l'accueil...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
21
app/login/page.tsx
Normal file
21
app/login/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const LoginPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Rediriger vers la page de connexion dans auth
|
||||
router.replace('/auth/login');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Redirection vers la connexion...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
21
app/materiel/page.tsx
Normal file
21
app/materiel/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const MaterielPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Rediriger vers la page principale du matériel dans (main)
|
||||
router.replace('/(main)/materiels');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Redirection vers le matériel...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterielPage;
|
||||
Reference in New Issue
Block a user