'use client'; import React, { useState, useEffect, useRef } from 'react'; import { Card } from 'primereact/card'; import { Button } from 'primereact/button'; import { Calendar } from 'primereact/calendar'; import { Chart } from 'primereact/chart'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Toast } from 'primereact/toast'; import { Toolbar } from 'primereact/toolbar'; import { Dropdown } from 'primereact/dropdown'; import { TabView, TabPanel } from 'primereact/tabview'; import { ProgressBar } from 'primereact/progressbar'; import { Tag } from 'primereact/tag'; import { InputText } from 'primereact/inputtext'; interface RentabiliteChantier { id: string; nom: string; client: string; dateDebut: Date; dateFin?: Date; statut: 'PLANIFIE' | 'EN_COURS' | 'TERMINE' | 'ANNULE'; budgetInitial: number; coutReel: number; chiffreAffaires: number; margeAbsolue: number; margeRelative: number; rentabilite: number; tempsPrevu: number; tempsReel: number; efficaciteTempo: number; risques: 'FAIBLE' | 'MOYEN' | 'ELEVE'; phase: string; } interface CoutCategorie { categorie: string; budgetPrevu: number; coutReel: number; ecart: number; pourcentage: number; } interface IndicateurPerformance { nom: string; valeur: number; objectif: number; unite: string; tendance: 'HAUSSE' | 'BAISSE' | 'STABLE'; } const RentabilitePage = () => { const [loading, setLoading] = useState(true); const [chantiers, setChantiers] = useState([]); const [coutCategories, setCoutCategories] = useState([]); const [indicateurs, setIndicateurs] = useState([]); const [selectedChantiers, setSelectedChantiers] = useState([]); const [dateDebut, setDateDebut] = useState(new Date(new Date().getFullYear(), 0, 1)); const [dateFin, setDateFin] = useState(new Date()); const [selectedPeriod, setSelectedPeriod] = useState('annee'); const [globalFilter, setGlobalFilter] = useState(''); const [activeIndex, setActiveIndex] = useState(0); const toast = useRef(null); const dt = useRef>(null); const periodOptions = [ { label: 'Ce mois', value: 'mois' }, { label: 'Ce trimestre', value: 'trimestre' }, { label: 'Cette année', value: 'annee' }, { label: 'Personnalisé', value: 'custom' } ]; useEffect(() => { loadRentabiliteData(); }, [dateDebut, dateFin]); const loadRentabiliteData = async () => { try { setLoading(true); // Données mockées const mockChantiers: RentabiliteChantier[] = [ { id: '1', nom: 'Résidence Les Palmiers', client: 'Kouassi Jean', dateDebut: new Date('2024-01-15'), dateFin: new Date('2024-06-20'), statut: 'TERMINE', budgetInitial: 800000, coutReel: 750000, chiffreAffaires: 950000, margeAbsolue: 200000, margeRelative: 21.05, rentabilite: 26.67, tempsPrevu: 150, tempsReel: 155, efficaciteTempo: 96.77, risques: 'FAIBLE', phase: 'Terminé' }, { id: '2', nom: 'Immeuble Commercial', client: 'Traoré Fatou', dateDebut: new Date('2024-03-01'), statut: 'EN_COURS', budgetInitial: 1200000, coutReel: 600000, chiffreAffaires: 700000, margeAbsolue: 100000, margeRelative: 14.29, rentabilite: 16.67, tempsPrevu: 300, tempsReel: 180, efficaciteTempo: 60.00, risques: 'MOYEN', phase: 'Gros œuvre' }, { id: '3', nom: 'Villa Moderne', client: 'Diabaté Mamadou', dateDebut: new Date('2024-04-10'), statut: 'EN_COURS', budgetInitial: 450000, coutReel: 280000, chiffreAffaires: 320000, margeAbsolue: 40000, margeRelative: 12.50, rentabilite: 14.29, tempsPrevu: 120, tempsReel: 85, efficaciteTempo: 70.83, risques: 'ELEVE', phase: 'Second œuvre' }, { id: '4', nom: 'Rénovation Bureau', client: 'Koné Mariame', dateDebut: new Date('2024-05-01'), dateFin: new Date('2024-05-25'), statut: 'TERMINE', budgetInitial: 180000, coutReel: 165000, chiffreAffaires: 220000, margeAbsolue: 55000, margeRelative: 25.00, rentabilite: 33.33, tempsPrevu: 25, tempsReel: 24, efficaciteTempo: 104.17, risques: 'FAIBLE', phase: 'Terminé' } ]; const mockCoutCategories: CoutCategorie[] = [ { categorie: 'Matériaux', budgetPrevu: 1500000, coutReel: 1420000, ecart: -80000, pourcentage: 45.2 }, { categorie: 'Main d\'œuvre', budgetPrevu: 900000, coutReel: 950000, ecart: 50000, pourcentage: 30.3 }, { categorie: 'Équipement', budgetPrevu: 400000, coutReel: 380000, ecart: -20000, pourcentage: 12.1 }, { categorie: 'Transport', budgetPrevu: 200000, coutReel: 220000, ecart: 20000, pourcentage: 7.0 }, { categorie: 'Autres', budgetPrevu: 180000, coutReel: 170000, ecart: -10000, pourcentage: 5.4 } ]; const mockIndicateurs: IndicateurPerformance[] = [ { nom: 'Marge moyenne', valeur: 18.71, objectif: 20.0, unite: '%', tendance: 'HAUSSE' }, { nom: 'Délai respect', valeur: 85.5, objectif: 90.0, unite: '%', tendance: 'STABLE' }, { nom: 'Efficacité coût', valeur: 92.3, objectif: 95.0, unite: '%', tendance: 'HAUSSE' }, { nom: 'Satisfaction client', valeur: 88.0, objectif: 85.0, unite: '%', tendance: 'HAUSSE' } ]; setChantiers(mockChantiers); setCoutCategories(mockCoutCategories); setIndicateurs(mockIndicateurs); } catch (error) { console.error('Erreur lors du chargement:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de charger les données de rentabilité', life: 3000 }); } finally { setLoading(false); } }; const onPeriodChange = (e: any) => { setSelectedPeriod(e.value); const now = new Date(); let debut = new Date(); switch (e.value) { case 'mois': debut = new Date(now.getFullYear(), now.getMonth(), 1); break; case 'trimestre': debut = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1); break; case 'annee': debut = new Date(now.getFullYear(), 0, 1); break; default: return; } setDateDebut(debut); setDateFin(now); }; const exportPDF = () => { toast.current?.show({ severity: 'info', summary: 'Export PDF', detail: 'Génération du rapport PDF...', life: 3000 }); }; const exportExcel = () => { dt.current?.exportCSV(); }; const leftToolbarTemplate = () => { return (
{selectedPeriod === 'custom' && ( <> setDateDebut(e.value || new Date())} dateFormat="dd/mm/yy" placeholder="Date début" /> setDateFin(e.value || new Date())} dateFormat="dd/mm/yy" placeholder="Date fin" /> )}
); }; const rightToolbarTemplate = () => { return (
); }; const formatCurrency = (amount: number) => { return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amount); }; const statutBodyTemplate = (rowData: RentabiliteChantier) => { let severity: "success" | "warning" | "danger" | "info" = 'info'; let label = rowData.statut; switch (rowData.statut) { case 'TERMINE': severity = 'success'; label = 'Terminé'; break; case 'EN_COURS': severity = 'warning'; label = 'En cours'; break; case 'PLANIFIE': severity = 'info'; label = 'Planifié'; break; case 'ANNULE': severity = 'danger'; label = 'Annulé'; break; } return ; }; const risqueBodyTemplate = (rowData: RentabiliteChantier) => { let severity: "success" | "warning" | "danger" = 'success'; let label = rowData.risques; switch (rowData.risques) { case 'FAIBLE': severity = 'success'; label = 'Faible'; break; case 'MOYEN': severity = 'warning'; label = 'Moyen'; break; case 'ELEVE': severity = 'danger'; label = 'Élevé'; break; } return ; }; const margeBodyTemplate = (rowData: RentabiliteChantier) => { const color = rowData.margeRelative >= 20 ? 'text-green-600' : rowData.margeRelative >= 10 ? 'text-orange-600' : 'text-red-600'; return {rowData.margeRelative.toFixed(2)}%; }; const rentabiliteBodyTemplate = (rowData: RentabiliteChantier) => { const color = rowData.rentabilite >= 25 ? 'text-green-600' : rowData.rentabilite >= 15 ? 'text-orange-600' : 'text-red-600'; return {rowData.rentabilite.toFixed(2)}%; }; const efficaciteBodyTemplate = (rowData: RentabiliteChantier) => { return (
= 95 ? '#10B981' : rowData.efficaciteTempo >= 80 ? '#F59E0B' : '#EF4444'} /> {rowData.efficaciteTempo.toFixed(1)}%
); }; const dateBodyTemplate = (rowData: RentabiliteChantier) => { return rowData.dateDebut.toLocaleDateString('fr-FR'); }; const header = (
Rentabilité des Chantiers
setGlobalFilter(e.currentTarget.value)} />
); // Calculs pour les indicateurs globaux const totalChiffreAffaires = chantiers.reduce((sum, c) => sum + c.chiffreAffaires, 0); const totalCouts = chantiers.reduce((sum, c) => sum + c.coutReel, 0); const margeGlobale = totalChiffreAffaires - totalCouts; const tauxMargeGlobal = totalChiffreAffaires > 0 ? (margeGlobale / totalChiffreAffaires) * 100 : 0; const nbChantiersRentables = chantiers.filter(c => c.rentabilite >= 15).length; const tauxRentabilite = chantiers.length > 0 ? (nbChantiersRentables / chantiers.length) * 100 : 0; // Données pour les graphiques const rentabiliteChartData = { labels: chantiers.map(c => c.nom), datasets: [ { label: 'Rentabilité (%)', data: chantiers.map(c => c.rentabilite), backgroundColor: chantiers.map(c => c.rentabilite >= 25 ? '#10B981' : c.rentabilite >= 15 ? '#F59E0B' : '#EF4444' ), borderColor: chantiers.map(c => c.rentabilite >= 25 ? '#047857' : c.rentabilite >= 15 ? '#D97706' : '#DC2626' ), borderWidth: 1 } ] }; const coutRepartitionData = { labels: coutCategories.map(c => c.categorie), datasets: [ { data: coutCategories.map(c => c.pourcentage), backgroundColor: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'], hoverBackgroundColor: ['#2563EB', '#059669', '#D97706', '#DC2626', '#7C3AED'] } ] }; const chartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' as const } } }; return (
setActiveIndex(e.index)}>
{/* Indicateurs principaux */}
{formatCurrency(margeGlobale)}
Marge Globale
{tauxMargeGlobal.toFixed(1)}%
Taux de Marge
{nbChantiersRentables}/{chantiers.length}
Chantiers Rentables
{tauxRentabilite.toFixed(1)}%
Taux Rentabilité
{/* Indicateurs de performance */}
{indicateurs.map((indicateur, index) => (
{indicateur.nom}
= indicateur.objectif ? '#10B981' : '#F59E0B'} />
{indicateur.valeur}{indicateur.unite} Obj: {indicateur.objectif}{indicateur.unite}
))}
{/* Graphiques */}
setSelectedChantiers(e.value)} dataKey="id" paginator rows={10} rowsPerPageOptions={[5, 10, 25]} className="datatable-responsive" paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown" currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} chantiers" globalFilter={globalFilter} emptyMessage="Aucun chantier trouvé." header={header} loading={loading} > formatCurrency(rowData.budgetInitial)} sortable /> formatCurrency(rowData.coutReel)} sortable /> formatCurrency(rowData.chiffreAffaires)} sortable /> formatCurrency(rowData.margeAbsolue)} sortable />
formatCurrency(rowData.budgetPrevu)} sortable /> formatCurrency(rowData.coutReel)} sortable /> ( = 0 ? 'text-red-500' : 'text-green-500'}> {formatCurrency(rowData.ecart)} )} sortable /> `${rowData.pourcentage.toFixed(1)}%`} sortable /> { const performance = rowData.budgetPrevu > 0 ? ((rowData.budgetPrevu - rowData.coutReel) / rowData.budgetPrevu) * 100 : 0; return (
= 0 ? '#10B981' : '#EF4444'} /> = 0 ? 'text-green-500' : 'text-red-500'}> {performance >= 0 ? 'Économie' : 'Dépassement'}: {Math.abs(performance).toFixed(1)}%
); }} />
c.categorie), datasets: [ { label: 'Budget Prévu', data: coutCategories.map(c => c.budgetPrevu), backgroundColor: '#3B82F6', borderColor: '#1D4ED8', borderWidth: 1 }, { label: 'Coût Réel', data: coutCategories.map(c => c.coutReel), backgroundColor: '#EF4444', borderColor: '#DC2626', borderWidth: 1 } ] }} options={{ ...chartOptions, scales: { y: { beginAtZero: true, ticks: { callback: function(value: any) { return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(value); } } } } }} style={{ height: '400px' }} />
); }; export default RentabilitePage;