786 lines
38 KiB
TypeScript
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; |