Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,647 @@
'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 { Knob } from 'primereact/knob';
import { ProgressBar } from 'primereact/progressbar';
import { Tag } from 'primereact/tag';
interface CAData {
periode: string;
chiffreAffaires: number;
objectif: number;
factures: number;
devis: number;
croissance: number;
tauxReussite: number;
}
interface CADetail {
id: string;
date: Date;
client: string;
chantier: string;
montant: number;
type: 'FACTURE' | 'DEVIS_ACCEPTE' | 'AVENANT';
statut: 'PAYE' | 'EN_ATTENTE' | 'RETARD';
mode: 'VIREMENT' | 'CHEQUE' | 'ESPECES' | 'CARTE';
}
const ChiffreAffairesPage = () => {
const [loading, setLoading] = useState(true);
const [caData, setCaData] = useState<CAData[]>([]);
const [caDetails, setCaDetails] = useState<CADetail[]>([]);
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 [selectedView, setSelectedView] = useState('mensuel');
const [globalFilter, setGlobalFilter] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<CADetail[]>>(null);
const periodOptions = [
{ label: 'Cette semaine', value: 'semaine' },
{ label: 'Ce mois', value: 'mois' },
{ label: 'Ce trimestre', value: 'trimestre' },
{ label: 'Cette année', value: 'annee' },
{ label: 'Personnalisé', value: 'custom' }
];
const viewOptions = [
{ label: 'Vue Mensuelle', value: 'mensuel' },
{ label: 'Vue Trimestrielle', value: 'trimestriel' },
{ label: 'Vue Annuelle', value: 'annuel' }
];
useEffect(() => {
loadCAData();
}, [dateDebut, dateFin, selectedView]);
const loadCAData = async () => {
try {
setLoading(true);
// Données mockées
const mockCAData: CAData[] = [
{ periode: 'Janvier 2024', chiffreAffaires: 145000, objectif: 150000, factures: 12, devis: 8, croissance: 12.5, tauxReussite: 75 },
{ periode: 'Février 2024', chiffreAffaires: 165000, objectif: 160000, factures: 15, devis: 10, croissance: 13.8, tauxReussite: 82 },
{ periode: 'Mars 2024', chiffreAffaires: 180000, objectif: 170000, factures: 18, devis: 12, croissance: 9.1, tauxReussite: 85 },
{ periode: 'Avril 2024', chiffreAffaires: 175000, objectif: 175000, factures: 16, devis: 11, croissance: -2.8, tauxReussite: 78 },
{ periode: 'Mai 2024', chiffreAffaires: 195000, objectif: 180000, factures: 20, devis: 14, croissance: 11.4, tauxReussite: 88 },
{ periode: 'Juin 2024', chiffreAffaires: 210000, objectif: 185000, factures: 22, devis: 16, croissance: 7.7, tauxReussite: 90 }
];
const mockCADetails: CADetail[] = [
{
id: '1',
date: new Date('2024-06-15'),
client: 'Kouassi Jean',
chantier: 'Résidence Les Palmiers',
montant: 25000,
type: 'FACTURE',
statut: 'PAYE',
mode: 'VIREMENT'
},
{
id: '2',
date: new Date('2024-06-10'),
client: 'Traoré Fatou',
chantier: 'Immeuble Commercial',
montant: 35000,
type: 'FACTURE',
statut: 'EN_ATTENTE',
mode: 'VIREMENT'
},
{
id: '3',
date: new Date('2024-06-08'),
client: 'Diabaté Mamadou',
chantier: 'Villa Moderne',
montant: 18000,
type: 'DEVIS_ACCEPTE',
statut: 'PAYE',
mode: 'CHEQUE'
},
{
id: '4',
date: new Date('2024-06-05'),
client: 'Koné Mariame',
chantier: 'Rénovation Bureau',
montant: 12000,
type: 'FACTURE',
statut: 'RETARD',
mode: 'VIREMENT'
},
{
id: '5',
date: new Date('2024-06-02'),
client: 'Ouattara Ibrahim',
chantier: 'Garage Automobile',
montant: 28000,
type: 'AVENANT',
statut: 'PAYE',
mode: 'VIREMENT'
}
];
setCaData(mockCAData);
setCaDetails(mockCADetails);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données du chiffre d\'affaires',
life: 3000
});
} finally {
setLoading(false);
}
};
const onPeriodChange = (e: any) => {
setSelectedPeriod(e.value);
const now = new Date();
let debut = new Date();
switch (e.value) {
case 'semaine':
debut = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
break;
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 = () => {
toast.current?.show({
severity: 'info',
summary: 'Export Excel',
detail: 'Génération du rapport Excel...',
life: 3000
});
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2 align-items-center">
<Dropdown
value={selectedPeriod}
options={periodOptions}
onChange={onPeriodChange}
placeholder="Période"
/>
<Dropdown
value={selectedView}
options={viewOptions}
onChange={(e) => setSelectedView(e.value)}
placeholder="Vue"
/>
{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={loadCAData}
/>
</div>
);
};
// Calculs pour les indicateurs
const totalCA = caData.reduce((sum, item) => sum + item.chiffreAffaires, 0);
const totalObjectif = caData.reduce((sum, item) => sum + item.objectif, 0);
const totalFactures = caData.reduce((sum, item) => sum + item.factures, 0);
const moyenneCroissance = caData.length > 0 ? caData.reduce((sum, item) => sum + item.croissance, 0) / caData.length : 0;
const tauxAtteinte = totalObjectif > 0 ? (totalCA / totalObjectif) * 100 : 0;
// Données pour les graphiques
const chartData = {
labels: caData.map(item => item.periode),
datasets: [
{
label: 'Chiffre d\'affaires',
data: caData.map(item => item.chiffreAffaires),
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Objectif',
data: caData.map(item => item.objectif),
borderColor: '#EF4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderDash: [5, 5],
tension: 0.4,
fill: false
}
]
};
const barChartData = {
labels: caData.map(item => item.periode),
datasets: [
{
label: 'Factures',
data: caData.map(item => item.factures),
backgroundColor: '#10B981',
borderColor: '#047857',
borderWidth: 1
},
{
label: 'Devis',
data: caData.map(item => item.devis),
backgroundColor: '#F59E0B',
borderColor: '#D97706',
borderWidth: 1
}
]
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom' as const
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value: any) {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(value);
}
}
}
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(amount);
};
const typeBodyTemplate = (rowData: CADetail) => {
let severity: "success" | "warning" | "danger" | "info" = 'info';
let label = rowData.type;
switch (rowData.type) {
case 'FACTURE':
severity = 'success';
label = 'Facture';
break;
case 'DEVIS_ACCEPTE':
severity = 'info';
label = 'Devis Accepté';
break;
case 'AVENANT':
severity = 'warning';
label = 'Avenant';
break;
}
return <Tag value={label} severity={severity} />;
};
const statutBodyTemplate = (rowData: CADetail) => {
let severity: "success" | "warning" | "danger" = 'success';
let label = rowData.statut;
switch (rowData.statut) {
case 'PAYE':
severity = 'success';
label = 'Payé';
break;
case 'EN_ATTENTE':
severity = 'warning';
label = 'En attente';
break;
case 'RETARD':
severity = 'danger';
label = 'En retard';
break;
}
return <Tag value={label} severity={severity} />;
};
const montantBodyTemplate = (rowData: CADetail) => {
return formatCurrency(rowData.montant);
};
const dateBodyTemplate = (rowData: CADetail) => {
return rowData.date.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">Détail des Transactions</h5>
<span className="block mt-2 md:mt-0 p-input-icon-left">
<i className="pi pi-search" />
<input
type="search"
placeholder="Rechercher..."
onChange={(e) => setGlobalFilter(e.target.value)}
className="p-inputtext p-component"
/>
</span>
</div>
);
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-primary mb-2">
<i className="pi pi-money-bill"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{formatCurrency(totalCA)}
</div>
<div className="text-lg text-color-secondary">
Chiffre d'Affaires
</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-target"></i>
</div>
<div className="text-3xl font-bold text-orange-500 mb-1">
{formatCurrency(totalObjectif)}
</div>
<div className="text-lg text-color-secondary">
Objectif
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-file"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{totalFactures}
</div>
<div className="text-lg text-color-secondary">
Factures
</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">
{moyenneCroissance.toFixed(1)}%
</div>
<div className="text-lg text-color-secondary">
Croissance Moyenne
</div>
</Card>
</div>
{/* Taux d'atteinte objectif */}
<div className="col-12 md:col-6">
<Card title="Taux d'Atteinte des Objectifs">
<div className="text-center">
<Knob
value={tauxAtteinte}
size={200}
valueColor={tauxAtteinte >= 100 ? "#10B981" : tauxAtteinte >= 80 ? "#F59E0B" : "#EF4444"}
rangeColor="#E5E7EB"
textColor="#374151"
/>
<div className="text-lg text-color-secondary mt-2">
{tauxAtteinte.toFixed(1)}% de l'objectif atteint
</div>
</div>
</Card>
</div>
{/* Performance mensuelle */}
<div className="col-12 md:col-6">
<Card title="Performance Mensuelle">
<div className="grid">
{caData.slice(-3).map((month, index) => (
<div key={index} className="col-12">
<div className="flex justify-content-between align-items-center mb-2">
<span className="font-semibold">{month.periode}</span>
<span className={`font-bold ${month.croissance >= 0 ? 'text-green-500' : 'text-red-500'}`}>
{month.croissance >= 0 ? '+' : ''}{month.croissance.toFixed(1)}%
</span>
</div>
<ProgressBar value={(month.chiffreAffaires / month.objectif) * 100} />
<div className="flex justify-content-between text-sm text-color-secondary mt-1">
<span>{formatCurrency(month.chiffreAffaires)}</span>
<span>Obj: {formatCurrency(month.objectif)}</span>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Graphique évolution */}
<div className="col-12">
<Card title="Évolution du Chiffre d'Affaires">
<Chart
type="line"
data={chartData}
options={chartOptions}
style={{ height: '400px' }}
/>
</Card>
</div>
{/* Graphique factures/devis */}
<div className="col-12">
<Card title="Volume Factures vs Devis">
<Chart
type="bar"
data={barChartData}
options={chartOptions}
style={{ height: '400px' }}
/>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Détail Transactions" leftIcon="pi pi-list mr-2">
<div className="grid">
<div className="col-12">
<Card>
<DataTable
ref={dt}
value={caDetails}
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} transactions"
globalFilter={globalFilter}
emptyMessage="Aucune transaction trouvée."
header={header}
loading={loading}
>
<Column field="date" header="Date" body={dateBodyTemplate} sortable />
<Column field="client" header="Client" sortable />
<Column field="chantier" header="Chantier" sortable />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="montant" header="Montant" body={montantBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="mode" header="Mode Paiement" sortable />
</DataTable>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Comparative" leftIcon="pi pi-chart-bar mr-2">
<div className="grid">
<div className="col-12">
<Card title="Comparaison Périodique">
<DataTable
value={caData}
loading={loading}
emptyMessage="Aucune donnée"
>
<Column field="periode" header="Période" sortable />
<Column
field="chiffreAffaires"
header="CA Réalisé"
body={(rowData) => formatCurrency(rowData.chiffreAffaires)}
sortable
/>
<Column
field="objectif"
header="Objectif"
body={(rowData) => formatCurrency(rowData.objectif)}
sortable
/>
<Column
field="ecart"
header="Écart"
body={(rowData) => {
const ecart = rowData.chiffreAffaires - rowData.objectif;
return (
<span className={ecart >= 0 ? 'text-green-500' : 'text-red-500'}>
{formatCurrency(ecart)}
</span>
);
}}
sortable
/>
<Column
field="tauxAtteinte"
header="% Objectif"
body={(rowData) => {
const taux = (rowData.chiffreAffaires / rowData.objectif) * 100;
return (
<span className={taux >= 100 ? 'text-green-500' : taux >= 80 ? 'text-orange-500' : 'text-red-500'}>
{taux.toFixed(1)}%
</span>
);
}}
sortable
/>
<Column
field="croissance"
header="Croissance"
body={(rowData) => (
<span className={rowData.croissance >= 0 ? 'text-green-500' : 'text-red-500'}>
{rowData.croissance >= 0 ? '+' : ''}{rowData.croissance.toFixed(1)}%
</span>
)}
sortable
/>
<Column
field="tauxReussite"
header="Taux Réussite"
body={(rowData) => (
<ProgressBar value={rowData.tauxReussite} showValue={false} />
)}
/>
</DataTable>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</Card>
</div>
</div>
);
};
export default ChiffreAffairesPage;

View File

@@ -0,0 +1,887 @@
'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 { Rating } from 'primereact/rating';
import { Tag } from 'primereact/tag';
import { InputText } from 'primereact/inputtext';
import { ProgressBar } from 'primereact/progressbar';
interface ClientAnalyse {
id: string;
nom: string;
prenom: string;
entreprise: string;
email: string;
telephone: string;
dateCreation: Date;
dernierContact: Date;
nbChantiers: number;
chantiersActifs: number;
chantiersTermines: number;
chiffreAffairesTotal: number;
chiffreAffairesAnnee: number;
moyennePanier: number;
satisfactionMoyenne: number;
delaiPaiementMoyen: number;
statut: 'ACTIF' | 'INACTIF' | 'PROSPECT' | 'VIP';
segment: 'PREMIUM' | 'STANDARD' | 'ECONOMIQUE';
risque: 'FAIBLE' | 'MOYEN' | 'ELEVE';
fidelite: number;
recommandations: number;
}
interface SegmentAnalyse {
segment: string;
nbClients: number;
chiffreAffaires: number;
pourcentageCA: number;
panierMoyen: number;
satisfaction: number;
fidelite: number;
}
interface TendanceClient {
mois: string;
nouveauxClients: number;
clientsActifs: number;
clientsPerdus: number;
chiffreAffaires: number;
satisfaction: number;
}
const SuiviClientsPage = () => {
const [loading, setLoading] = useState(true);
const [clients, setClients] = useState<ClientAnalyse[]>([]);
const [segments, setSegments] = useState<SegmentAnalyse[]>([]);
const [tendances, setTendances] = useState<TendanceClient[]>([]);
const [selectedClients, setSelectedClients] = useState<ClientAnalyse[]>([]);
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 [selectedSegment, setSelectedSegment] = useState('tous');
const [globalFilter, setGlobalFilter] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<ClientAnalyse[]>>(null);
const periodOptions = [
{ label: 'Ce mois', value: 'mois' },
{ label: 'Ce trimestre', value: 'trimestre' },
{ label: 'Cette année', value: 'annee' },
{ label: 'Personnalisé', value: 'custom' }
];
const segmentOptions = [
{ label: 'Tous les segments', value: 'tous' },
{ label: 'Premium', value: 'PREMIUM' },
{ label: 'Standard', value: 'STANDARD' },
{ label: 'Économique', value: 'ECONOMIQUE' }
];
useEffect(() => {
loadClientData();
}, [dateDebut, dateFin, selectedSegment]);
const loadClientData = async () => {
try {
setLoading(true);
// Données mockées
const mockClients: ClientAnalyse[] = [
{
id: '1',
nom: 'Kouassi',
prenom: 'Jean',
entreprise: 'Entreprise Kouassi',
email: 'jean.kouassi@email.com',
telephone: '07 12 34 56 78',
dateCreation: new Date('2023-01-15'),
dernierContact: new Date('2024-06-10'),
nbChantiers: 8,
chantiersActifs: 2,
chantiersTermines: 6,
chiffreAffairesTotal: 2500000,
chiffreAffairesAnnee: 950000,
moyennePanier: 312500,
satisfactionMoyenne: 4.5,
delaiPaiementMoyen: 28,
statut: 'VIP',
segment: 'PREMIUM',
risque: 'FAIBLE',
fidelite: 92,
recommandations: 3
},
{
id: '2',
nom: 'Traoré',
prenom: 'Fatou',
entreprise: 'Traoré SARL',
email: 'fatou.traore@email.com',
telephone: '07 98 76 54 32',
dateCreation: new Date('2023-06-20'),
dernierContact: new Date('2024-05-25'),
nbChantiers: 3,
chantiersActifs: 1,
chantiersTermines: 2,
chiffreAffairesTotal: 850000,
chiffreAffairesAnnee: 420000,
moyennePanier: 283333,
satisfactionMoyenne: 4.2,
delaiPaiementMoyen: 32,
statut: 'ACTIF',
segment: 'STANDARD',
risque: 'FAIBLE',
fidelite: 78,
recommandations: 1
},
{
id: '3',
nom: 'Diabaté',
prenom: 'Mamadou',
entreprise: 'Diabaté & Fils',
email: 'mamadou.diabate@email.com',
telephone: '07 55 44 33 22',
dateCreation: new Date('2024-02-10'),
dernierContact: new Date('2024-06-05'),
nbChantiers: 2,
chantiersActifs: 1,
chantiersTermines: 1,
chiffreAffairesTotal: 320000,
chiffreAffairesAnnee: 320000,
moyennePanier: 160000,
satisfactionMoyenne: 3.8,
delaiPaiementMoyen: 45,
statut: 'ACTIF',
segment: 'ECONOMIQUE',
risque: 'MOYEN',
fidelite: 65,
recommandations: 0
},
{
id: '4',
nom: 'Koné',
prenom: 'Mariame',
entreprise: 'Bureau Koné',
email: 'mariame.kone@email.com',
telephone: '07 11 22 33 44',
dateCreation: new Date('2023-09-05'),
dernierContact: new Date('2024-01-20'),
nbChantiers: 1,
chantiersActifs: 0,
chantiersTermines: 1,
chiffreAffairesTotal: 220000,
chiffreAffairesAnnee: 0,
moyennePanier: 220000,
satisfactionMoyenne: 4.8,
delaiPaiementMoyen: 15,
statut: 'INACTIF',
segment: 'STANDARD',
risque: 'ELEVE',
fidelite: 45,
recommandations: 1
},
{
id: '5',
nom: 'Ouattara',
prenom: 'Ibrahim',
entreprise: 'Garage Ouattara',
email: 'ibrahim.ouattara@email.com',
telephone: '07 66 77 88 99',
dateCreation: new Date('2024-04-15'),
dernierContact: new Date('2024-06-15'),
nbChantiers: 1,
chantiersActifs: 1,
chantiersTermines: 0,
chiffreAffairesTotal: 180000,
chiffreAffairesAnnee: 180000,
moyennePanier: 180000,
satisfactionMoyenne: 4.0,
delaiPaiementMoyen: 30,
statut: 'PROSPECT',
segment: 'ECONOMIQUE',
risque: 'FAIBLE',
fidelite: 0,
recommandations: 0
}
];
const mockSegments: SegmentAnalyse[] = [
{
segment: 'PREMIUM',
nbClients: 1,
chiffreAffaires: 2500000,
pourcentageCA: 61.0,
panierMoyen: 312500,
satisfaction: 4.5,
fidelite: 92
},
{
segment: 'STANDARD',
nbClients: 2,
chiffreAffaires: 1070000,
pourcentageCA: 26.1,
panierMoyen: 251667,
satisfaction: 4.5,
fidelite: 61.5
},
{
segment: 'ECONOMIQUE',
nbClients: 2,
chiffreAffaires: 500000,
pourcentageCA: 12.2,
panierMoyen: 170000,
satisfaction: 3.9,
fidelite: 32.5
}
];
const mockTendances: TendanceClient[] = [
{ mois: 'Jan 2024', nouveauxClients: 2, clientsActifs: 3, clientsPerdus: 0, chiffreAffaires: 350000, satisfaction: 4.2 },
{ mois: 'Fév 2024', nouveauxClients: 1, clientsActifs: 4, clientsPerdus: 0, chiffreAffaires: 420000, satisfaction: 4.3 },
{ mois: 'Mar 2024', nouveauxClients: 0, clientsActifs: 4, clientsPerdus: 1, chiffreAffaires: 380000, satisfaction: 4.1 },
{ mois: 'Avr 2024', nouveauxClients: 1, clientsActifs: 4, clientsPerdus: 0, chiffreAffaires: 480000, satisfaction: 4.4 },
{ mois: 'Mai 2024', nouveauxClients: 1, clientsActifs: 5, clientsPerdus: 0, chiffreAffaires: 520000, satisfaction: 4.3 },
{ mois: 'Jun 2024', nouveauxClients: 0, clientsActifs: 4, clientsPerdus: 1, chiffreAffaires: 450000, satisfaction: 4.2 }
];
setClients(selectedSegment === 'tous' ? mockClients : mockClients.filter(c => c.segment === selectedSegment));
setSegments(mockSegments);
setTendances(mockTendances);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données clients',
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"
/>
<Dropdown
value={selectedSegment}
options={segmentOptions}
onChange={(e) => setSelectedSegment(e.value)}
placeholder="Segment"
/>
{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={loadClientData}
/>
</div>
);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(amount);
};
const statutBodyTemplate = (rowData: ClientAnalyse) => {
let severity: "success" | "warning" | "danger" | "info" = 'info';
let label = rowData.statut;
switch (rowData.statut) {
case 'VIP':
severity = 'success';
label = 'VIP';
break;
case 'ACTIF':
severity = 'info';
label = 'Actif';
break;
case 'INACTIF':
severity = 'warning';
label = 'Inactif';
break;
case 'PROSPECT':
severity = 'danger';
label = 'Prospect';
break;
}
return <Tag value={label} severity={severity} />;
};
const segmentBodyTemplate = (rowData: ClientAnalyse) => {
let severity: "success" | "warning" | "danger" = 'success';
let label = rowData.segment;
switch (rowData.segment) {
case 'PREMIUM':
severity = 'success';
label = 'Premium';
break;
case 'STANDARD':
severity = 'warning';
label = 'Standard';
break;
case 'ECONOMIQUE':
severity = 'danger';
label = 'Économique';
break;
}
return <Tag value={label} severity={severity} />;
};
const risqueBodyTemplate = (rowData: ClientAnalyse) => {
let severity: "success" | "warning" | "danger" = 'success';
let label = rowData.risque;
switch (rowData.risque) {
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 satisfactionBodyTemplate = (rowData: ClientAnalyse) => {
return <Rating value={rowData.satisfactionMoyenne} readOnly cancel={false} />;
};
const fideliteBodyTemplate = (rowData: ClientAnalyse) => {
return (
<div>
<ProgressBar value={rowData.fidelite} showValue={false} />
<small>{rowData.fidelite}%</small>
</div>
);
};
const dateBodyTemplate = (rowData: ClientAnalyse) => {
return rowData.dateCreation.toLocaleDateString('fr-FR');
};
const dernierContactBodyTemplate = (rowData: ClientAnalyse) => {
const daysDiff = Math.floor((new Date().getTime() - rowData.dernierContact.getTime()) / (1000 * 3600 * 24));
const color = daysDiff > 90 ? 'text-red-500' : daysDiff > 30 ? 'text-orange-500' : 'text-green-500';
return (
<div>
<div>{rowData.dernierContact.toLocaleDateString('fr-FR')}</div>
<small className={color}>Il y a {daysDiff} jours</small>
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Analyse Clients</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 totalClients = clients.length;
const clientsActifs = clients.filter(c => c.statut === 'ACTIF' || c.statut === 'VIP').length;
const totalCA = clients.reduce((sum, c) => sum + c.chiffreAffairesAnnee, 0);
const panierMoyenGlobal = totalClients > 0 ? totalCA / totalClients : 0;
const satisfactionMoyenne = totalClients > 0 ? clients.reduce((sum, c) => sum + c.satisfactionMoyenne, 0) / totalClients : 0;
const fideliteMoyenne = totalClients > 0 ? clients.reduce((sum, c) => sum + c.fidelite, 0) / totalClients : 0;
// Données pour les graphiques
const segmentChartData = {
labels: segments.map(s => s.segment),
datasets: [
{
data: segments.map(s => s.pourcentageCA),
backgroundColor: ['#10B981', '#F59E0B', '#EF4444'],
hoverBackgroundColor: ['#059669', '#D97706', '#DC2626']
}
]
};
const tendanceChartData = {
labels: tendances.map(t => t.mois),
datasets: [
{
label: 'Nouveaux Clients',
data: tendances.map(t => t.nouveauxClients),
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Clients Actifs',
data: tendances.map(t => t.clientsActifs),
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Clients Perdus',
data: tendances.map(t => t.clientsPerdus),
borderColor: '#EF4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
tension: 0.4,
fill: true
}
]
};
const caClientChartData = {
labels: tendances.map(t => t.mois),
datasets: [
{
label: 'CA par Client',
data: tendances.map(t => t.chiffreAffaires / Math.max(t.clientsActifs, 1)),
backgroundColor: '#8B5CF6',
borderColor: '#7C3AED',
borderWidth: 1
}
]
};
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-primary mb-2">
<i className="pi pi-users"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{totalClients}
</div>
<div className="text-lg text-color-secondary">
Total Clients
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-check-circle"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{clientsActifs}
</div>
<div className="text-lg text-color-secondary">
Clients Actifs
</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-money-bill"></i>
</div>
<div className="text-3xl font-bold text-orange-500 mb-1">
{formatCurrency(panierMoyenGlobal)}
</div>
<div className="text-lg text-color-secondary">
Panier Moyen
</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-star"></i>
</div>
<div className="text-3xl font-bold text-purple-500 mb-1">
{satisfactionMoyenne.toFixed(1)}/5
</div>
<div className="text-lg text-color-secondary">
Satisfaction Moyenne
</div>
</Card>
</div>
{/* Analyse par segment */}
<div className="col-12">
<Card title="Analyse par Segment">
<DataTable
value={segments}
loading={loading}
emptyMessage="Aucune donnée de segment"
>
<Column field="segment" header="Segment" />
<Column field="nbClients" header="Nb Clients" />
<Column
field="chiffreAffaires"
header="Chiffre d'Affaires"
body={(rowData) => formatCurrency(rowData.chiffreAffaires)}
/>
<Column
field="pourcentageCA"
header="% CA Total"
body={(rowData) => `${rowData.pourcentageCA.toFixed(1)}%`}
/>
<Column
field="panierMoyen"
header="Panier Moyen"
body={(rowData) => formatCurrency(rowData.panierMoyen)}
/>
<Column
field="satisfaction"
header="Satisfaction"
body={(rowData) => (
<Rating value={rowData.satisfaction} readOnly cancel={false} />
)}
/>
<Column
field="fidelite"
header="Fidélité"
body={(rowData) => (
<div>
<ProgressBar value={rowData.fidelite} showValue={false} />
<small>{rowData.fidelite.toFixed(1)}%</small>
</div>
)}
/>
</DataTable>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 md:col-4">
<Card title="Répartition CA par Segment">
<Chart
type="doughnut"
data={segmentChartData}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12 md:col-8">
<Card title="Évolution Clients">
<Chart
type="line"
data={tendanceChartData}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12">
<Card title="Chiffre d'Affaires par Client">
<Chart
type="bar"
data={caClientChartData}
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: '300px' }}
/>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Clients Détaillés" leftIcon="pi pi-list mr-2">
<div className="grid">
<div className="col-12">
<Card>
<DataTable
ref={dt}
value={clients}
selection={selectedClients}
onSelectionChange={(e) => setSelectedClients(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} clients"
globalFilter={globalFilter}
emptyMessage="Aucun client trouvé."
header={header}
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="prenom" header="Prénom" sortable />
<Column field="nom" header="Nom" sortable />
<Column field="entreprise" header="Entreprise" sortable />
<Column field="email" header="Email" sortable />
<Column field="telephone" header="Téléphone" />
<Column field="dateCreation" header="Création" body={dateBodyTemplate} sortable />
<Column field="dernierContact" header="Dernier Contact" body={dernierContactBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="segment" header="Segment" body={segmentBodyTemplate} sortable />
<Column field="nbChantiers" header="Chantiers" sortable />
<Column
field="chiffreAffairesTotal"
header="CA Total"
body={(rowData) => formatCurrency(rowData.chiffreAffairesTotal)}
sortable
/>
<Column
field="chiffreAffairesAnnee"
header="CA Année"
body={(rowData) => formatCurrency(rowData.chiffreAffairesAnnee)}
sortable
/>
<Column
field="moyennePanier"
header="Panier Moyen"
body={(rowData) => formatCurrency(rowData.moyennePanier)}
sortable
/>
<Column field="satisfactionMoyenne" header="Satisfaction" body={satisfactionBodyTemplate} sortable />
<Column field="delaiPaiementMoyen" header="Délai Paiement" body={(rowData) => `${rowData.delaiPaiementMoyen}j`} sortable />
<Column field="fidelite" header="Fidélité" body={fideliteBodyTemplate} sortable />
<Column field="risque" header="Risque" body={risqueBodyTemplate} sortable />
<Column field="recommandations" header="Recommandations" sortable />
</DataTable>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Comportementale" leftIcon="pi pi-chart-bar mr-2">
<div className="grid">
<div className="col-12 md:col-6">
<Card title="Clients par Niveau de Risque">
<div className="grid">
<div className="col-4 text-center">
<div className="text-3xl text-green-500 font-bold">
{clients.filter(c => c.risque === 'FAIBLE').length}
</div>
<div className="text-sm">Risque Faible</div>
</div>
<div className="col-4 text-center">
<div className="text-3xl text-orange-500 font-bold">
{clients.filter(c => c.risque === 'MOYEN').length}
</div>
<div className="text-sm">Risque Moyen</div>
</div>
<div className="col-4 text-center">
<div className="text-3xl text-red-500 font-bold">
{clients.filter(c => c.risque === 'ELEVE').length}
</div>
<div className="text-sm">Risque Élevé</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Distribution Fidélité">
<Chart
type="bar"
data={{
labels: ['0-25%', '26-50%', '51-75%', '76-100%'],
datasets: [
{
label: 'Nombre de clients',
data: [
clients.filter(c => c.fidelite <= 25).length,
clients.filter(c => c.fidelite > 25 && c.fidelite <= 50).length,
clients.filter(c => c.fidelite > 50 && c.fidelite <= 75).length,
clients.filter(c => c.fidelite > 75).length
],
backgroundColor: ['#EF4444', '#F59E0B', '#3B82F6', '#10B981'],
borderColor: ['#DC2626', '#D97706', '#2563EB', '#059669'],
borderWidth: 1
}
]
}}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12">
<Card title="Matrice Valeur-Fidélité">
<div className="text-center p-4">
<p className="text-color-secondary mb-4">
Analyse de la relation entre la valeur client (CA) et leur niveau de fidélité
</p>
<div className="grid">
{clients.map((client, index) => (
<div key={index} className="col-12 md:col-4 mb-3">
<Card className="text-center">
<div className="font-bold text-lg">{client.prenom} {client.nom}</div>
<div className="text-sm text-color-secondary mb-2">{client.entreprise}</div>
<div className="grid">
<div className="col-6">
<div className="text-primary font-bold">CA Total</div>
<div className="text-sm">{formatCurrency(client.chiffreAffairesTotal)}</div>
</div>
<div className="col-6">
<div className="text-orange-500 font-bold">Fidélité</div>
<div className="text-sm">{client.fidelite}%</div>
</div>
</div>
<div className="mt-2">
<ProgressBar value={client.fidelite} showValue={false} />
</div>
<div className="mt-2">
{segmentBodyTemplate(client)}
{' '}
{risqueBodyTemplate(client)}
</div>
</Card>
</div>
))}
</div>
</div>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</Card>
</div>
</div>
);
};
export default SuiviClientsPage;

View File

@@ -0,0 +1,984 @@
'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 { Rating } from 'primereact/rating';
import { Tag } from 'primereact/tag';
import { InputText } from 'primereact/inputtext';
import { ProgressBar } from 'primereact/progressbar';
import { Knob } from 'primereact/knob';
interface Employe {
id: string;
nom: string;
prenom: string;
poste: string;
equipe: string;
dateEmbauche: Date;
statut: 'ACTIF' | 'CONGE' | 'ARRET' | 'FORMATION';
heuresTravaillees: number;
heuresObjectif: number;
chantiersAssignes: number;
chantiersTermines: number;
tauxReussite: number;
evaluationPerformance: number;
salaireBase: number;
primes: number;
specialites: string[];
certifications: string[];
formation: number; // heures de formation
securite: number; // score sécurité
satisfaction: number; // satisfaction client
}
interface EquipePerformance {
nom: string;
chef: string;
nbMembres: number;
heuresTravaillees: number;
productivite: number;
chantiersAssignes: number;
chantiersTermines: number;
tauxReussite: number;
budgetRespect: number;
delaiRespect: number;
securiteScore: number;
satisfactionClient: number;
coutTotal: number;
chiffreAffaires: number;
rentabilite: number;
}
interface IndicateurRH {
nom: string;
valeurActuelle: number;
objectif: number;
unite: string;
tendance: 'HAUSSE' | 'BAISSE' | 'STABLE';
couleur: string;
}
const PerformanceEquipesPage = () => {
const [loading, setLoading] = useState(true);
const [employes, setEmployes] = useState<Employe[]>([]);
const [equipes, setEquipes] = useState<EquipePerformance[]>([]);
const [indicateursRH, setIndicateursRH] = useState<IndicateurRH[]>([]);
const [selectedEmployes, setSelectedEmployes] = useState<Employe[]>([]);
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 [selectedEquipe, setSelectedEquipe] = useState('toutes');
const [globalFilter, setGlobalFilter] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Employe[]>>(null);
const periodOptions = [
{ label: 'Ce mois', value: 'mois' },
{ label: 'Ce trimestre', value: 'trimestre' },
{ label: 'Cette année', value: 'annee' },
{ label: 'Personnalisé', value: 'custom' }
];
const equipeOptions = [
{ label: 'Toutes les équipes', value: 'toutes' },
{ label: 'Équipe Gros Œuvre', value: 'gros-oeuvre' },
{ label: 'Équipe Second Œuvre', value: 'second-oeuvre' },
{ label: 'Équipe Finitions', value: 'finitions' },
{ label: 'Équipe Maintenance', value: 'maintenance' }
];
useEffect(() => {
loadPerformanceData();
}, [dateDebut, dateFin, selectedEquipe]);
const loadPerformanceData = async () => {
try {
setLoading(true);
// Données mockées employés
const mockEmployes: Employe[] = [
{
id: '1',
nom: 'Martin',
prenom: 'Jean',
poste: 'Chef d\'équipe',
equipe: 'gros-oeuvre',
dateEmbauche: new Date('2022-01-15'),
statut: 'ACTIF',
heuresTravaillees: 1680,
heuresObjectif: 1800,
chantiersAssignes: 8,
chantiersTermines: 7,
tauxReussite: 87.5,
evaluationPerformance: 4.5,
salaireBase: 3200,
primes: 450,
specialites: ['Maçonnerie', 'Coffrage', 'Management'],
certifications: ['CACES R389', 'Sauveteur Secouriste'],
formation: 35,
securite: 95,
satisfaction: 4.3
},
{
id: '2',
nom: 'Dubois',
prenom: 'Marie',
poste: 'Maçon',
equipe: 'gros-oeuvre',
dateEmbauche: new Date('2023-03-20'),
statut: 'ACTIF',
heuresTravaillees: 1620,
heuresObjectif: 1750,
chantiersAssignes: 6,
chantiersTermines: 6,
tauxReussite: 100,
evaluationPerformance: 4.2,
salaireBase: 2800,
primes: 320,
specialites: ['Maçonnerie', 'Carrelage'],
certifications: ['Sauveteur Secouriste'],
formation: 28,
securite: 92,
satisfaction: 4.5
},
{
id: '3',
nom: 'Durand',
prenom: 'Pierre',
poste: 'Électricien',
equipe: 'second-oeuvre',
dateEmbauche: new Date('2021-09-10'),
statut: 'ACTIF',
heuresTravaillees: 1750,
heuresObjectif: 1800,
chantiersAssignes: 10,
chantiersTermines: 9,
tauxReussite: 90,
evaluationPerformance: 4.7,
salaireBase: 3000,
primes: 500,
specialites: ['Électricité', 'Domotique', 'Photovoltaïque'],
certifications: ['Habilitation B2V', 'QualiPV'],
formation: 42,
securite: 98,
satisfaction: 4.6
},
{
id: '4',
nom: 'Bernard',
prenom: 'Sophie',
poste: 'Plombier',
equipe: 'second-oeuvre',
dateEmbauche: new Date('2023-01-08'),
statut: 'FORMATION',
heuresTravaillees: 1580,
heuresObjectif: 1750,
chantiersAssignes: 5,
chantiersTermines: 4,
tauxReussite: 80,
evaluationPerformance: 3.8,
salaireBase: 2700,
primes: 180,
specialites: ['Plomberie', 'Chauffage'],
certifications: ['PGN'],
formation: 65,
securite: 88,
satisfaction: 4.1
},
{
id: '5',
nom: 'Moreau',
prenom: 'Luc',
poste: 'Peintre',
equipe: 'finitions',
dateEmbauche: new Date('2022-06-15'),
statut: 'ACTIF',
heuresTravaillees: 1650,
heuresObjectif: 1750,
chantiersAssignes: 12,
chantiersTermines: 11,
tauxReussite: 91.7,
evaluationPerformance: 4.1,
salaireBase: 2600,
primes: 275,
specialites: ['Peinture', 'Papier peint', 'Décoration'],
certifications: ['Peinture écologique'],
formation: 18,
securite: 90,
satisfaction: 4.4
}
];
const mockEquipes: EquipePerformance[] = [
{
nom: 'Équipe Gros Œuvre',
chef: 'Jean Martin',
nbMembres: 6,
heuresTravaillees: 9800,
productivite: 92,
chantiersAssignes: 15,
chantiersTermines: 14,
tauxReussite: 93.3,
budgetRespect: 88,
delaiRespect: 85,
securiteScore: 94,
satisfactionClient: 4.3,
coutTotal: 125000,
chiffreAffaires: 180000,
rentabilite: 30.6
},
{
nom: 'Équipe Second Œuvre',
chef: 'Pierre Durand',
nbMembres: 8,
heuresTravaillees: 13600,
productivite: 95,
chantiersAssignes: 20,
chantiersTermines: 18,
tauxReussite: 90,
budgetRespect: 92,
delaiRespect: 88,
securiteScore: 96,
satisfactionClient: 4.5,
coutTotal: 168000,
chiffreAffaires: 250000,
rentabilite: 32.8
},
{
nom: 'Équipe Finitions',
chef: 'Luc Moreau',
nbMembres: 4,
heuresTravaillees: 6800,
productivite: 89,
chantiersAssignes: 25,
chantiersTermines: 23,
tauxReussite: 92,
budgetRespect: 90,
delaiRespect: 95,
securiteScore: 91,
satisfactionClient: 4.6,
coutTotal: 88000,
chiffreAffaires: 135000,
rentabilite: 34.8
},
{
nom: 'Équipe Maintenance',
chef: 'Anne Petit',
nbMembres: 3,
heuresTravaillees: 5100,
productivite: 87,
chantiersAssignes: 18,
chantiersTermines: 17,
tauxReussite: 94.4,
budgetRespect: 95,
delaiRespect: 98,
securiteScore: 97,
satisfactionClient: 4.2,
coutTotal: 65000,
chiffreAffaires: 95000,
rentabilite: 31.6
}
];
const mockIndicateursRH: IndicateurRH[] = [
{ nom: 'Productivité Globale', valeurActuelle: 93.2, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#10B981' },
{ nom: 'Taux de Réussite', valeurActuelle: 92.4, objectif: 90, unite: '%', tendance: 'STABLE', couleur: '#3B82F6' },
{ nom: 'Respect Délais', valeurActuelle: 91.5, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#F59E0B' },
{ nom: 'Score Sécurité', valeurActuelle: 94.5, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#EF4444' },
{ nom: 'Satisfaction Client', valeurActuelle: 4.4, objectif: 4.5, unite: '/5', tendance: 'STABLE', couleur: '#8B5CF6' },
{ nom: 'Heures Formation', valeurActuelle: 37.6, objectif: 40, unite: 'h', tendance: 'HAUSSE', couleur: '#06B6D4' }
];
setEmployes(selectedEquipe === 'toutes' ? mockEmployes : mockEmployes.filter(e => e.equipe === selectedEquipe));
setEquipes(mockEquipes);
setIndicateursRH(mockIndicateursRH);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données de performance',
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"
/>
<Dropdown
value={selectedEquipe}
options={equipeOptions}
onChange={(e) => setSelectedEquipe(e.value)}
placeholder="Équipe"
/>
{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={loadPerformanceData}
/>
</div>
);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(amount);
};
const statutBodyTemplate = (rowData: Employe) => {
let severity: "success" | "warning" | "danger" | "info" = 'success';
let label = rowData.statut;
switch (rowData.statut) {
case 'ACTIF':
severity = 'success';
label = 'Actif';
break;
case 'CONGE':
severity = 'info';
label = 'Congé';
break;
case 'ARRET':
severity = 'danger';
label = 'Arrêt';
break;
case 'FORMATION':
severity = 'warning';
label = 'Formation';
break;
}
return <Tag value={label} severity={severity} />;
};
const performanceBodyTemplate = (rowData: Employe) => {
return <Rating value={rowData.evaluationPerformance} readOnly cancel={false} />;
};
const productiviteBodyTemplate = (rowData: Employe) => {
const productivite = (rowData.heuresTravaillees / rowData.heuresObjectif) * 100;
const color = productivite >= 95 ? 'text-green-600' : productivite >= 80 ? 'text-orange-600' : 'text-red-600';
return (
<div>
<ProgressBar value={productivite} showValue={false} />
<small className={color}>{productivite.toFixed(1)}%</small>
</div>
);
};
const reussiteBodyTemplate = (rowData: Employe) => {
const color = rowData.tauxReussite >= 90 ? 'text-green-600' :
rowData.tauxReussite >= 75 ? 'text-orange-600' : 'text-red-600';
return <span className={color}>{rowData.tauxReussite.toFixed(1)}%</span>;
};
const securiteBodyTemplate = (rowData: Employe) => {
return (
<div>
<ProgressBar value={rowData.securite} showValue={false} />
<small>{rowData.securite}%</small>
</div>
);
};
const specialitesBodyTemplate = (rowData: Employe) => {
return rowData.specialites.slice(0, 2).map((spec, index) => (
<Tag key={index} value={spec} className="mr-1 mb-1" severity="info" />
));
};
const dateEmbaucheBodyTemplate = (rowData: Employe) => {
const anciennete = Math.floor((new Date().getTime() - rowData.dateEmbauche.getTime()) / (1000 * 3600 * 24 * 365));
return (
<div>
<div>{rowData.dateEmbauche.toLocaleDateString('fr-FR')}</div>
<small className="text-color-secondary">{anciennete} an{anciennete > 1 ? 's' : ''}</small>
</div>
);
};
const salaireBodyTemplate = (rowData: Employe) => {
return (
<div>
<div>{formatCurrency(rowData.salaireBase)}</div>
{rowData.primes > 0 && (
<small className="text-green-500">+{formatCurrency(rowData.primes)} primes</small>
)}
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Performance des Équipes</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 totalEmployes = employes.length;
const employesActifs = employes.filter(e => e.statut === 'ACTIF').length;
const heuresTotal = employes.reduce((sum, e) => sum + e.heuresTravaillees, 0);
const heuresObjectifTotal = employes.reduce((sum, e) => sum + e.heuresObjectif, 0);
const productiviteGlobale = heuresObjectifTotal > 0 ? (heuresTotal / heuresObjectifTotal) * 100 : 0;
const performanceMoyenne = totalEmployes > 0 ? employes.reduce((sum, e) => sum + e.evaluationPerformance, 0) / totalEmployes : 0;
// Données pour les graphiques
const equipePerformanceData = {
labels: equipes.map(e => e.nom),
datasets: [
{
label: 'Productivité (%)',
data: equipes.map(e => e.productivite),
backgroundColor: '#3B82F6',
borderColor: '#1D4ED8',
borderWidth: 1
},
{
label: 'Taux Réussite (%)',
data: equipes.map(e => e.tauxReussite),
backgroundColor: '#10B981',
borderColor: '#047857',
borderWidth: 1
}
]
};
const rentabiliteEquipeData = {
labels: equipes.map(e => e.nom),
datasets: [
{
data: equipes.map(e => e.rentabilite),
backgroundColor: ['#10B981', '#3B82F6', '#F59E0B', '#EF4444'],
hoverBackgroundColor: ['#059669', '#2563EB', '#D97706', '#DC2626']
}
]
};
const tendanceFormationData = {
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'],
datasets: [
{
label: 'Heures Formation',
data: [32, 28, 35, 42, 38, 45],
borderColor: '#8B5CF6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
tension: 0.4,
fill: true
}
]
};
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-primary mb-2">
<i className="pi pi-users"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{totalEmployes}
</div>
<div className="text-lg text-color-secondary">
Total Employés
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-check-circle"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{employesActifs}
</div>
<div className="text-lg text-color-secondary">
Employés Actifs
</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-chart-line"></i>
</div>
<div className="text-3xl font-bold text-orange-500 mb-1">
{productiviteGlobale.toFixed(1)}%
</div>
<div className="text-lg text-color-secondary">
Productivité Globale
</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-star"></i>
</div>
<div className="text-3xl font-bold text-purple-500 mb-1">
{performanceMoyenne.toFixed(1)}/5
</div>
<div className="text-lg text-color-secondary">
Performance Moyenne
</div>
</Card>
</div>
{/* Indicateurs RH */}
<div className="col-12">
<Card title="Indicateurs RH">
<div className="grid">
{indicateursRH.map((indicateur, index) => (
<div key={index} className="col-12 md:col-6 lg:col-4">
<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>
<Knob
value={indicateur.valeurActuelle}
max={indicateur.unite === '/5' ? 5 : 100}
size={100}
valueColor={indicateur.couleur}
rangeColor="#E5E7EB"
textColor="#374151"
/>
<div className="mt-2">
<div className="font-bold">{indicateur.valeurActuelle}{indicateur.unite}</div>
<div className="text-sm text-color-secondary">
Objectif: {indicateur.objectif}{indicateur.unite}
</div>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Performance par équipe */}
<div className="col-12">
<Card title="Performance par Équipe">
<DataTable
value={equipes}
loading={loading}
emptyMessage="Aucune équipe"
>
<Column field="nom" header="Équipe" />
<Column field="chef" header="Chef d'équipe" />
<Column field="nbMembres" header="Membres" />
<Column
field="productivite"
header="Productivité"
body={(rowData) => (
<div>
<ProgressBar value={rowData.productivite} showValue={false} />
<small>{rowData.productivite}%</small>
</div>
)}
/>
<Column
field="tauxReussite"
header="Taux Réussite"
body={(rowData) => `${rowData.tauxReussite.toFixed(1)}%`}
/>
<Column
field="delaiRespect"
header="Respect Délais"
body={(rowData) => `${rowData.delaiRespect}%`}
/>
<Column
field="securiteScore"
header="Sécurité"
body={(rowData) => `${rowData.securiteScore}%`}
/>
<Column
field="satisfactionClient"
header="Satisfaction"
body={(rowData) => (
<Rating value={rowData.satisfactionClient} readOnly cancel={false} />
)}
/>
<Column
field="rentabilite"
header="Rentabilité"
body={(rowData) => `${rowData.rentabilite.toFixed(1)}%`}
/>
</DataTable>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 md:col-8">
<Card title="Comparaison Performance Équipes">
<Chart
type="bar"
data={equipePerformanceData}
options={chartOptions}
style={{ height: '400px' }}
/>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="Rentabilité par Équipe">
<Chart
type="doughnut"
data={rentabiliteEquipeData}
options={chartOptions}
style={{ height: '400px' }}
/>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Employés Détaillés" leftIcon="pi pi-list mr-2">
<div className="grid">
<div className="col-12">
<Card>
<DataTable
ref={dt}
value={employes}
selection={selectedEmployes}
onSelectionChange={(e) => setSelectedEmployes(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} employés"
globalFilter={globalFilter}
emptyMessage="Aucun employé trouvé."
header={header}
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="prenom" header="Prénom" sortable />
<Column field="nom" header="Nom" sortable />
<Column field="poste" header="Poste" sortable />
<Column field="equipe" header="Équipe" sortable />
<Column field="dateEmbauche" header="Embauche" body={dateEmbaucheBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="productivite" header="Productivité" body={productiviteBodyTemplate} />
<Column field="chantiersTermines" header="Chantiers" body={(rowData) => `${rowData.chantiersTermines}/${rowData.chantiersAssignes}`} sortable />
<Column field="tauxReussite" header="Réussite" body={reussiteBodyTemplate} sortable />
<Column field="evaluationPerformance" header="Performance" body={performanceBodyTemplate} sortable />
<Column field="securite" header="Sécurité" body={securiteBodyTemplate} sortable />
<Column field="formation" header="Formation (h)" sortable />
<Column field="specialites" header="Spécialités" body={specialitesBodyTemplate} />
<Column field="salaireBase" header="Salaire" body={salaireBodyTemplate} sortable />
</DataTable>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Formation & Développement" leftIcon="pi pi-graduation-cap mr-2">
<div className="grid">
<div className="col-12 md:col-6">
<Card title="Évolution Formation">
<Chart
type="line"
data={tendanceFormationData}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Répartition Formations par Employé">
<Chart
type="bar"
data={{
labels: employes.map(e => `${e.prenom} ${e.nom}`),
datasets: [
{
label: 'Heures Formation',
data: employes.map(e => e.formation),
backgroundColor: '#8B5CF6',
borderColor: '#7C3AED',
borderWidth: 1
}
]
}}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12">
<Card title="Compétences et Certifications">
<DataTable
value={employes}
loading={loading}
>
<Column field="prenom" header="Prénom" />
<Column field="nom" header="Nom" />
<Column field="poste" header="Poste" />
<Column
field="specialites"
header="Spécialités"
body={(rowData) => rowData.specialites.join(', ')}
/>
<Column
field="certifications"
header="Certifications"
body={(rowData) => rowData.certifications.join(', ')}
/>
<Column
field="formation"
header="Formation (h)"
body={(rowData) => (
<div>
<ProgressBar value={(rowData.formation / 80) * 100} showValue={false} />
<small>{rowData.formation}h / 80h objectif</small>
</div>
)}
/>
<Column
field="evaluationPerformance"
header="Performance"
body={performanceBodyTemplate}
/>
</DataTable>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Coûts RH" leftIcon="pi pi-money-bill mr-2">
<div className="grid">
<div className="col-12 md:col-4">
<Card title="Coût Total Masse Salariale">
<div className="text-center">
<div className="text-3xl font-bold text-primary mb-2">
{formatCurrency(employes.reduce((sum, e) => sum + e.salaireBase + e.primes, 0))}
</div>
<div className="text-color-secondary">
Salaires + Primes mensuel
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="Coût par Heure">
<div className="text-center">
<div className="text-3xl font-bold text-green-500 mb-2">
{heuresTotal > 0 ?
(employes.reduce((sum, e) => sum + e.salaireBase + e.primes, 0) / heuresTotal).toFixed(2) :
'0.00'
}
</div>
<div className="text-color-secondary">
Coût horaire moyen
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="ROI Formation">
<div className="text-center">
<div className="text-3xl font-bold text-orange-500 mb-2">
+12.5%
</div>
<div className="text-color-secondary">
Amélioration productivité
</div>
</div>
</Card>
</div>
<div className="col-12">
<Card title="Analyse Coûts par Employé">
<DataTable
value={employes}
loading={loading}
>
<Column field="prenom" header="Prénom" />
<Column field="nom" header="Nom" />
<Column field="poste" header="Poste" />
<Column
field="salaireBase"
header="Salaire Base"
body={(rowData) => formatCurrency(rowData.salaireBase)}
sortable
/>
<Column
field="primes"
header="Primes"
body={(rowData) => formatCurrency(rowData.primes)}
sortable
/>
<Column
field="coutTotal"
header="Coût Total"
body={(rowData) => formatCurrency(rowData.salaireBase + rowData.primes)}
sortable
/>
<Column
field="coutHeure"
header="Coût/Heure"
body={(rowData) =>
`${((rowData.salaireBase + rowData.primes) / rowData.heuresTravaillees).toFixed(2)}`
}
/>
<Column
field="productivite"
header="Productivité"
body={productiviteBodyTemplate}
/>
<Column
field="rentabilite"
header="Rentabilité"
body={(rowData) => {
const ca = rowData.chantiersTermines * 50000; // CA estimé
const cout = rowData.salaireBase + rowData.primes;
const rentabilite = ca > 0 ? ((ca - cout) / ca) * 100 : 0;
return `${rentabilite.toFixed(1)}%`;
}}
/>
</DataTable>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</Card>
</div>
</div>
);
};
export default PerformanceEquipesPage;

View File

@@ -0,0 +1,703 @@
'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 { Knob } from 'primereact/knob';
import { ProgressBar } from 'primereact/progressbar';
import { Badge } from 'primereact/badge';
import { chantierService, clientService, factureService, devisService } from '../../../services/api';
import { formatCurrency, formatDate } from '../../../utils/formatters';
import type { Chantier, Client, Facture, Devis } from '../../../types/btp';
interface ReportData {
chantiers: Chantier[];
clients: Client[];
factures: Facture[];
devis: Devis[];
}
interface ChantierStats {
total: number;
planifies: number;
enCours: number;
termines: number;
annules: number;
enRetard: number;
}
interface FinancialStats {
chiffreAffaires: number;
benefices: number;
facturesEnAttente: number;
devisEnAttente: number;
tauxReussite: number;
}
const RapportsPage = () => {
const [reportData, setReportData] = useState<ReportData>({
chantiers: [],
clients: [],
factures: [],
devis: []
});
const [loading, setLoading] = useState(true);
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 [activeIndex, setActiveIndex] = useState(0);
const [chantierStats, setChantierStats] = useState<ChantierStats>({
total: 0,
planifies: 0,
enCours: 0,
termines: 0,
annules: 0,
enRetard: 0
});
const [financialStats, setFinancialStats] = useState<FinancialStats>({
chiffreAffaires: 0,
benefices: 0,
facturesEnAttente: 0,
devisEnAttente: 0,
tauxReussite: 0
});
const [chartData, setChartData] = useState<any>({});
const [chartOptions, setChartOptions] = useState<any>({});
const toast = useRef<Toast>(null);
const periodOptions = [
{ label: 'Cette semaine', value: 'semaine' },
{ label: 'Ce mois', value: 'mois' },
{ label: 'Ce trimestre', value: 'trimestre' },
{ label: 'Cette année', value: 'annee' },
{ label: 'Personnalisé', value: 'custom' }
];
useEffect(() => {
loadReportData();
}, [dateDebut, dateFin]);
useEffect(() => {
calculateStats();
generateChartData();
}, [reportData]);
const loadReportData = async () => {
try {
setLoading(true);
// Simuler des données de rapport
const mockChantiers: Chantier[] = [
{
id: '1',
nom: 'Résidence Les Palmiers',
description: 'Construction de 20 appartements',
adresse: '123 Rue des Palmiers, Abidjan',
dateDebut: new Date('2024-01-15'),
dateFinPrevue: new Date('2024-06-15'),
dateFinReelle: new Date('2024-06-20'),
statut: 'TERMINE',
montantPrevu: 850000,
montantReel: 820000,
actif: true,
client: {
id: '1',
nom: 'Kouassi',
prenom: 'Jean',
email: 'jean.kouassi@email.com',
telephone: '07 12 34 56 78',
adresse: '456 Rue de la Paix',
codePostal: '00225',
ville: 'Abidjan',
entreprise: 'Entreprise Kouassi',
dateCreation: new Date('2024-01-01'),
dateModification: new Date('2024-01-01'),
actif: true
}
},
{
id: '2',
nom: 'Immeuble Commercial',
description: 'Bureaux commerciaux',
adresse: '789 Boulevard Principal, Abidjan',
dateDebut: new Date('2024-03-01'),
dateFinPrevue: new Date('2024-12-31'),
dateFinReelle: null,
statut: 'EN_COURS',
montantPrevu: 1200000,
montantReel: 600000,
actif: true,
client: {
id: '2',
nom: 'Traoré',
prenom: 'Fatou',
email: 'fatou.traore@email.com',
telephone: '07 98 76 54 32',
adresse: '321 Avenue du Commerce',
codePostal: '00225',
ville: 'Abidjan',
entreprise: 'Traoré SARL',
dateCreation: new Date('2024-02-01'),
dateModification: new Date('2024-02-01'),
actif: true
}
}
];
setReportData({
chantiers: mockChantiers,
clients: [],
factures: [],
devis: []
});
} 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 calculateStats = () => {
const { chantiers } = reportData;
const stats: ChantierStats = {
total: chantiers.length,
planifies: chantiers.filter(c => c.statut === 'PLANIFIE').length,
enCours: chantiers.filter(c => c.statut === 'EN_COURS').length,
termines: chantiers.filter(c => c.statut === 'TERMINE').length,
annules: chantiers.filter(c => c.statut === 'ANNULE').length,
enRetard: chantiers.filter(c => {
if (!c.dateFinPrevue) return false;
const now = new Date();
return new Date(c.dateFinPrevue) < now && c.statut !== 'TERMINE';
}).length
};
setChantierStats(stats);
const financialStats: FinancialStats = {
chiffreAffaires: chantiers.reduce((sum, c) => sum + (c.montantReel || 0), 0),
benefices: chantiers.reduce((sum, c) => sum + ((c.montantReel || 0) - (c.montantPrevu || 0)), 0),
facturesEnAttente: 0,
devisEnAttente: 0,
tauxReussite: Math.round((stats.termines / Math.max(stats.total, 1)) * 100)
};
setFinancialStats(financialStats);
};
const generateChartData = () => {
const { chantiers } = reportData;
// Graphique en secteurs - Statuts des chantiers
const pieData = {
labels: ['Planifiés', 'En cours', 'Terminés', 'Annulés'],
datasets: [
{
data: [
chantierStats.planifies,
chantierStats.enCours,
chantierStats.termines,
chantierStats.annules
],
backgroundColor: [
'#3B82F6',
'#10B981',
'#6B7280',
'#EF4444'
],
borderColor: [
'#1D4ED8',
'#047857',
'#374151',
'#DC2626'
],
borderWidth: 1
}
]
};
// Graphique en barres - Évolution mensuelle
const barData = {
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'],
datasets: [
{
label: 'Chiffre d\'affaires',
data: [120000, 150000, 180000, 200000, 250000, 300000, 280000, 320000, 350000, 380000, 400000, 450000],
backgroundColor: '#3B82F6',
borderColor: '#1D4ED8',
borderWidth: 1
},
{
label: 'Bénéfices',
data: [20000, 25000, 30000, 35000, 40000, 45000, 42000, 48000, 52000, 55000, 58000, 62000],
backgroundColor: '#10B981',
borderColor: '#047857',
borderWidth: 1
}
]
};
setChartData({ pie: pieData, bar: barData });
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
};
setChartOptions(options);
};
const onPeriodChange = (e: any) => {
setSelectedPeriod(e.value);
const now = new Date();
let debut = new Date();
switch (e.value) {
case 'semaine':
debut = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
break;
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 = () => {
toast.current?.show({
severity: 'info',
summary: 'Export Excel',
detail: 'Génération du rapport Excel...',
life: 3000
});
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2 align-items-center">
<Dropdown
value={selectedPeriod}
options={periodOptions}
onChange={onPeriodChange}
placeholder="Sélectionner une 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={loadReportData}
/>
</div>
);
};
const renderVueEnsemble = () => {
return (
<div className="grid">
{/* Indicateurs principaux */}
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-primary mb-2">
<i className="pi pi-building"></i>
</div>
<div className="text-3xl font-bold text-primary mb-1">
{chantierStats.total}
</div>
<div className="text-lg text-color-secondary">
Chantiers Total
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-green-500 mb-2">
<i className="pi pi-check-circle"></i>
</div>
<div className="text-3xl font-bold text-green-500 mb-1">
{chantierStats.termines}
</div>
<div className="text-lg text-color-secondary">
Chantiers Terminés
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-yellow-500 mb-2">
<i className="pi pi-clock"></i>
</div>
<div className="text-3xl font-bold text-yellow-500 mb-1">
{chantierStats.enCours}
</div>
<div className="text-lg text-color-secondary">
En Cours
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-6xl text-red-500 mb-2">
<i className="pi pi-exclamation-triangle"></i>
</div>
<div className="text-3xl font-bold text-red-500 mb-1">
{chantierStats.enRetard}
</div>
<div className="text-lg text-color-secondary">
En Retard
</div>
</Card>
</div>
{/* Indicateurs financiers */}
<div className="col-12 md:col-6">
<Card title="Performance Financière">
<div className="grid">
<div className="col-12 md:col-6">
<div className="text-center">
<div className="text-2xl font-bold text-primary mb-2">
{formatCurrency(financialStats.chiffreAffaires)}
</div>
<div className="text-color-secondary">
Chiffre d'Affaires
</div>
</div>
</div>
<div className="col-12 md:col-6">
<div className="text-center">
<div className="text-2xl font-bold text-green-500 mb-2">
{formatCurrency(financialStats.benefices)}
</div>
<div className="text-color-secondary">
Bénéfices
</div>
</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Taux de Réussite">
<div className="text-center">
<Knob
value={financialStats.tauxReussite}
size={150}
valueColor="#10B981"
rangeColor="#E5E7EB"
textColor="#374151"
/>
<div className="text-lg text-color-secondary mt-2">
Projets Terminés avec Succès
</div>
</div>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 md:col-6">
<Card title="Répartition des Chantiers">
<Chart
type="pie"
data={chartData.pie}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Évolution Mensuelle">
<Chart
type="bar"
data={chartData.bar}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
</div>
);
};
const renderRapportChantiers = () => {
return (
<div className="grid">
<div className="col-12">
<Card title="Rapport Détaillé des Chantiers">
<DataTable
value={reportData.chantiers}
paginator
rows={10}
dataKey="id"
loading={loading}
emptyMessage="Aucun chantier trouvé"
>
<Column field="nom" header="Nom" sortable />
<Column
field="client"
header="Client"
body={(rowData) => rowData.client ?
`${rowData.client.prenom} ${rowData.client.nom}` :
'Non défini'
}
/>
<Column
field="dateDebut"
header="Date Début"
body={(rowData) => formatDate(rowData.dateDebut)}
sortable
/>
<Column
field="dateFinPrevue"
header="Date Fin Prévue"
body={(rowData) => formatDate(rowData.dateFinPrevue)}
sortable
/>
<Column
field="statut"
header="Statut"
body={(rowData) => (
<Badge
value={rowData.statut}
severity={
rowData.statut === 'TERMINE' ? 'success' :
rowData.statut === 'EN_COURS' ? 'info' :
rowData.statut === 'PLANIFIE' ? 'warning' : 'danger'
}
/>
)}
/>
<Column
field="montantPrevu"
header="Montant Prévu"
body={(rowData) => formatCurrency(rowData.montantPrevu)}
sortable
/>
<Column
field="montantReel"
header="Montant Réel"
body={(rowData) => formatCurrency(rowData.montantReel || 0)}
sortable
/>
<Column
field="avancement"
header="Avancement"
body={(rowData) => {
const progress = rowData.statut === 'TERMINE' ? 100 :
rowData.statut === 'EN_COURS' ? 50 :
rowData.statut === 'PLANIFIE' ? 0 : 0;
return <ProgressBar value={progress} />;
}}
/>
</DataTable>
</Card>
</div>
</div>
);
};
const renderRapportFinancier = () => {
return (
<div className="grid">
<div className="col-12 md:col-4">
<Card title="Revenus">
<div className="text-center">
<div className="text-3xl font-bold text-green-500 mb-2">
{formatCurrency(financialStats.chiffreAffaires)}
</div>
<div className="text-color-secondary">
Total des revenus
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="Bénéfices">
<div className="text-center">
<div className="text-3xl font-bold text-primary mb-2">
{formatCurrency(financialStats.benefices)}
</div>
<div className="text-color-secondary">
Total des bénéfices
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card title="Marge">
<div className="text-center">
<div className="text-3xl font-bold text-yellow-500 mb-2">
{financialStats.chiffreAffaires > 0 ?
`${Math.round((financialStats.benefices / financialStats.chiffreAffaires) * 100)}%` :
'0%'
}
</div>
<div className="text-color-secondary">
Marge bénéficiaire
</div>
</div>
</Card>
</div>
<div className="col-12">
<Card title="Analyse Financière Détaillée">
<DataTable
value={reportData.chantiers}
paginator
rows={10}
dataKey="id"
loading={loading}
emptyMessage="Aucune donnée financière"
>
<Column field="nom" header="Chantier" sortable />
<Column
field="montantPrevu"
header="Budget Initial"
body={(rowData) => formatCurrency(rowData.montantPrevu)}
sortable
/>
<Column
field="montantReel"
header="Coût Réel"
body={(rowData) => formatCurrency(rowData.montantReel || 0)}
sortable
/>
<Column
field="ecart"
header="Écart"
body={(rowData) => {
const ecart = (rowData.montantReel || 0) - rowData.montantPrevu;
return (
<span className={ecart >= 0 ? 'text-green-500' : 'text-red-500'}>
{formatCurrency(ecart)}
</span>
);
}}
/>
<Column
field="rentabilite"
header="Rentabilité"
body={(rowData) => {
const rentabilite = rowData.montantPrevu > 0 ?
(((rowData.montantReel || 0) - rowData.montantPrevu) / rowData.montantPrevu * 100) : 0;
return (
<span className={rentabilite >= 0 ? 'text-green-500' : 'text-red-500'}>
{rentabilite.toFixed(1)}%
</span>
);
}}
/>
</DataTable>
</Card>
</div>
</div>
);
};
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">
{renderVueEnsemble()}
</TabPanel>
<TabPanel header="Rapport Chantiers" leftIcon="pi pi-building mr-2">
{renderRapportChantiers()}
</TabPanel>
<TabPanel header="Rapport Financier" leftIcon="pi pi-money-bill mr-2">
{renderRapportFinancier()}
</TabPanel>
</TabView>
</Card>
</div>
</div>
);
};
export default RapportsPage;

View File

@@ -0,0 +1,786 @@
'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;