Compare commits

...

5 Commits

38 changed files with 10116 additions and 383 deletions

View File

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

File diff suppressed because it is too large Load Diff

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;

View File

@@ -38,4 +38,4 @@
"exclude": [
"node_modules"
]
}
}