Compare commits

...

5 Commits

38 changed files with 10116 additions and 383 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -560,4 +560,8 @@ const ExecutionGranulaireChantier = () => {
);
};
<<<<<<< HEAD
export default ExecutionGranulaireChantier;
=======
export default ExecutionGranulaireChantier;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -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

View File

@@ -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

View File

@@ -374,6 +374,12 @@ const ChantiersExecutionGranulaire = () => {
);
};
<<<<<<< HEAD
export default ChantiersExecutionGranulaire;
=======
export default ChantiersExecutionGranulaire;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

File diff suppressed because it is too large Load Diff

View File

@@ -375,5 +375,10 @@ const ChantiersPlanifiesPage = () => {
};
export default ChantiersPlanifiesPage;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -473,5 +473,10 @@ STATISTIQUES:
};
export default ChantiersRetardPage;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -403,5 +403,10 @@ Variance budgétaire: ${formatCurrency(totalCost - totalBudget)} (${Math.round((
};
export default ChantiersTerminesPage;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -581,7 +581,14 @@ const WorkflowChantiers = () => {
};
export default WorkflowChantiers;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -487,5 +487,10 @@ const RechercheClientPage = () => {
);
};
<<<<<<< HEAD
export default RechercheClientPage;
=======
export default RechercheClientPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -535,4 +535,8 @@ className="w-full md:w-14rem"
};
export default DashboardMaintenance;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -718,4 +718,8 @@ const DashboardRessources = () => {
};
export default DashboardRessources;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -607,6 +607,12 @@ RECOMMANDATIONS:
);
};
<<<<<<< HEAD
export default FacturesPayeesPage;
=======
export default FacturesPayeesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -712,6 +712,12 @@ ACTIONS RECOMMANDÉES:
);
};
<<<<<<< HEAD
export default FacturesRetardPage;
=======
export default FacturesRetardPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -310,5 +310,10 @@ const MaintenancesEnCoursPage = () => {
);
};
<<<<<<< HEAD
export default MaintenancesEnCoursPage;
=======
export default MaintenancesEnCoursPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -445,5 +445,10 @@ const MaintenancesPlanifieesPage = () => {
);
};
<<<<<<< HEAD
export default MaintenancesPlanifieesPage;
=======
export default MaintenancesPlanifieesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -324,5 +324,10 @@ const MaterielsDisponiblesPage = () => {
);
};
<<<<<<< HEAD
export default MaterielsDisponiblesPage;
=======
export default MaterielsDisponiblesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -339,5 +339,10 @@ const MaintenancePrevuePage = () => {
);
};
<<<<<<< HEAD
export default MaintenancePrevuePage;
=======
export default MaintenancePrevuePage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -255,4 +255,8 @@ const RechercheMaterielsPage = () => {
);
};
<<<<<<< HEAD
export default RechercheMaterielsPage;
=======
export default RechercheMaterielsPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -551,4 +551,8 @@ const LandingPage: Page = () => {
};
export default LandingPage;
<<<<<<< HEAD
=======
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -494,4 +494,8 @@ const PhasesEnRetardPage: Page = () => {
);
};
<<<<<<< HEAD
export default PhasesEnRetardPage;
=======
export default PhasesEnRetardPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -1010,6 +1010,12 @@ const CalendrierPlanningPage = () => {
);
};
<<<<<<< HEAD
export default CalendrierPlanningPage;
=======
export default CalendrierPlanningPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -1351,6 +1351,12 @@ const EquipesPage = () => {
);
};
<<<<<<< HEAD
export default EquipesPage;
=======
export default EquipesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -1549,6 +1549,12 @@ const MaterielPage = () => {
);
};
<<<<<<< HEAD
export default MaterielPage;
=======
export default MaterielPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -836,5 +836,10 @@ const CommandesPage = () => {
);
};
<<<<<<< HEAD
export default CommandesPage;
=======
export default CommandesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

View File

@@ -1022,5 +1022,10 @@ const SortiesPage = () => {
);
};
<<<<<<< HEAD
export default SortiesPage;
=======
export default SortiesPage;
>>>>>>> 5ed9a80339c59d689b9655e8be1a68312d19d4a1

21
app/clients/page.tsx Normal file
View 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
View 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
View 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
View 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;