Files
btpxpress-frontend/app/(main)/rapports/rentabilite/page.tsx
2025-10-01 01:39:07 +00:00

786 lines
38 KiB
TypeScript

'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<RentabiliteChantier[]>([]);
const [coutCategories, setCoutCategories] = useState<CoutCategorie[]>([]);
const [indicateurs, setIndicateurs] = useState<IndicateurPerformance[]>([]);
const [selectedChantiers, setSelectedChantiers] = useState<RentabiliteChantier[]>([]);
const [dateDebut, setDateDebut] = useState<Date>(new Date(new Date().getFullYear(), 0, 1));
const [dateFin, setDateFin] = useState<Date>(new Date());
const [selectedPeriod, setSelectedPeriod] = useState('annee');
const [globalFilter, setGlobalFilter] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<RentabiliteChantier[]>>(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 (
<div className="flex flex-wrap gap-2 align-items-center">
<Dropdown
value={selectedPeriod}
options={periodOptions}
onChange={onPeriodChange}
placeholder="Période"
/>
{selectedPeriod === 'custom' && (
<>
<Calendar
value={dateDebut}
onChange={(e) => setDateDebut(e.value || new Date())}
dateFormat="dd/mm/yy"
placeholder="Date début"
/>
<Calendar
value={dateFin}
onChange={(e) => setDateFin(e.value || new Date())}
dateFormat="dd/mm/yy"
placeholder="Date fin"
/>
</>
)}
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="PDF"
icon="pi pi-file-pdf"
severity="danger"
onClick={exportPDF}
/>
<Button
label="Excel"
icon="pi pi-file-excel"
severity="success"
onClick={exportExcel}
/>
<Button
label="Actualiser"
icon="pi pi-refresh"
onClick={loadRentabiliteData}
/>
</div>
);
};
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 <Tag value={label} severity={severity} />;
};
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 <Tag value={label} severity={severity} />;
};
const margeBodyTemplate = (rowData: RentabiliteChantier) => {
const color = rowData.margeRelative >= 20 ? 'text-green-600' :
rowData.margeRelative >= 10 ? 'text-orange-600' : 'text-red-600';
return <span className={color}>{rowData.margeRelative.toFixed(2)}%</span>;
};
const rentabiliteBodyTemplate = (rowData: RentabiliteChantier) => {
const color = rowData.rentabilite >= 25 ? 'text-green-600' :
rowData.rentabilite >= 15 ? 'text-orange-600' : 'text-red-600';
return <span className={color}>{rowData.rentabilite.toFixed(2)}%</span>;
};
const efficaciteBodyTemplate = (rowData: RentabiliteChantier) => {
return (
<div>
<ProgressBar
value={rowData.efficaciteTempo}
showValue={false}
color={rowData.efficaciteTempo >= 95 ? '#10B981' :
rowData.efficaciteTempo >= 80 ? '#F59E0B' : '#EF4444'}
/>
<small>{rowData.efficaciteTempo.toFixed(1)}%</small>
</div>
);
};
const dateBodyTemplate = (rowData: RentabiliteChantier) => {
return rowData.dateDebut.toLocaleDateString('fr-FR');
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Rentabilité des Chantiers</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>
);
// 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 (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
<TabPanel header="Vue d'Ensemble" leftIcon="pi pi-chart-line mr-2">
<div className="grid">
{/* Indicateurs principaux */}
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-money-bill"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{formatCurrency(margeGlobale)}
</div>
<div className="text-lg text-color-secondary">
Marge Globale
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-primary mb-2">
<i className="pi pi-percentage"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{tauxMargeGlobal.toFixed(1)}%
</div>
<div className="text-lg text-color-secondary">
Taux de Marge
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-orange-500 mb-2">
<i className="pi pi-building"></i>
</div>
<div className="text-3xl font-bold text-orange-500 mb-1">
{nbChantiersRentables}/{chantiers.length}
</div>
<div className="text-lg text-color-secondary">
Chantiers Rentables
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-purple-500 mb-2">
<i className="pi pi-trending-up"></i>
</div>
<div className="text-3xl font-bold text-purple-500 mb-1">
{tauxRentabilite.toFixed(1)}%
</div>
<div className="text-lg text-color-secondary">
Taux Rentabilité
</div>
</Card>
</div>
{/* Indicateurs de performance */}
<div className="col-12">
<Card title="Indicateurs de Performance">
<div className="grid">
{indicateurs.map((indicateur, index) => (
<div key={index} className="col-12 md:col-3">
<div className="text-center p-3">
<div className="flex justify-content-between align-items-center mb-2">
<span className="font-semibold">{indicateur.nom}</span>
<i className={`pi ${
indicateur.tendance === 'HAUSSE' ? 'pi-trending-up text-green-500' :
indicateur.tendance === 'BAISSE' ? 'pi-trending-down text-red-500' :
'pi-minus text-orange-500'
}`}></i>
</div>
<ProgressBar
value={(indicateur.valeur / indicateur.objectif) * 100}
showValue={false}
color={indicateur.valeur >= indicateur.objectif ? '#10B981' : '#F59E0B'}
/>
<div className="flex justify-content-between text-sm mt-1">
<span className="font-bold">{indicateur.valeur}{indicateur.unite}</span>
<span className="text-color-secondary">Obj: {indicateur.objectif}{indicateur.unite}</span>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 md:col-8">
<Card title="Rentabilité par Chantier">
<Chart
type="bar"
data={rentabiliteChartData}
options={{
...chartOptions,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value: any) {
return value + '%';
}
}
}
}
}}
style={{ height: '400px' }}
/>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="Répartition des Coûts">
<Chart
type="doughnut"
data={coutRepartitionData}
options={chartOptions}
style={{ height: '400px' }}
/>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Détaillée" leftIcon="pi pi-list mr-2">
<div className="grid">
<div className="col-12">
<Card>
<DataTable
ref={dt}
value={chantiers}
selection={selectedChantiers}
onSelectionChange={(e) => 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}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="nom" header="Chantier" sortable />
<Column field="client" header="Client" sortable />
<Column field="dateDebut" header="Date début" body={dateBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="phase" header="Phase" sortable />
<Column
field="budgetInitial"
header="Budget Initial"
body={(rowData) => formatCurrency(rowData.budgetInitial)}
sortable
/>
<Column
field="coutReel"
header="Coût Réel"
body={(rowData) => formatCurrency(rowData.coutReel)}
sortable
/>
<Column
field="chiffreAffaires"
header="CA"
body={(rowData) => formatCurrency(rowData.chiffreAffaires)}
sortable
/>
<Column
field="margeAbsolue"
header="Marge"
body={(rowData) => formatCurrency(rowData.margeAbsolue)}
sortable
/>
<Column field="margeRelative" header="Marge %" body={margeBodyTemplate} sortable />
<Column field="rentabilite" header="Rentabilité %" body={rentabiliteBodyTemplate} sortable />
<Column field="efficaciteTempo" header="Efficacité Temps" body={efficaciteBodyTemplate} />
<Column field="risques" header="Niveau Risque" body={risqueBodyTemplate} sortable />
</DataTable>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Coûts" leftIcon="pi pi-chart-pie mr-2">
<div className="grid">
<div className="col-12">
<Card title="Analyse des Coûts par Catégorie">
<DataTable
value={coutCategories}
loading={loading}
emptyMessage="Aucune donnée de coût"
>
<Column field="categorie" header="Catégorie" sortable />
<Column
field="budgetPrevu"
header="Budget Prévu"
body={(rowData) => formatCurrency(rowData.budgetPrevu)}
sortable
/>
<Column
field="coutReel"
header="Coût Réel"
body={(rowData) => formatCurrency(rowData.coutReel)}
sortable
/>
<Column
field="ecart"
header="Écart"
body={(rowData) => (
<span className={rowData.ecart >= 0 ? 'text-red-500' : 'text-green-500'}>
{formatCurrency(rowData.ecart)}
</span>
)}
sortable
/>
<Column
field="pourcentage"
header="% Total"
body={(rowData) => `${rowData.pourcentage.toFixed(1)}%`}
sortable
/>
<Column
field="performance"
header="Performance"
body={(rowData) => {
const performance = rowData.budgetPrevu > 0 ?
((rowData.budgetPrevu - rowData.coutReel) / rowData.budgetPrevu) * 100 : 0;
return (
<div>
<ProgressBar
value={Math.abs(performance)}
showValue={false}
color={performance >= 0 ? '#10B981' : '#EF4444'}
/>
<small className={performance >= 0 ? 'text-green-500' : 'text-red-500'}>
{performance >= 0 ? 'Économie' : 'Dépassement'}: {Math.abs(performance).toFixed(1)}%
</small>
</div>
);
}}
/>
</DataTable>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Évolution des Coûts">
<Chart
type="line"
data={{
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'],
datasets: [
{
label: 'Budget Prévu',
data: [400000, 450000, 520000, 580000, 620000, 680000],
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderDash: [5, 5]
},
{
label: 'Coût Réel',
data: [380000, 440000, 510000, 590000, 635000, 695000],
borderColor: '#EF4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
tension: 0.4
}
]
}}
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' }}
/>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Comparaison Budget vs Réel">
<Chart
type="bar"
data={{
labels: coutCategories.map(c => 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' }}
/>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</Card>
</div>
</div>
);
};
export default RentabilitePage;