Initial commit
This commit is contained in:
647
app/(main)/rapports/ca/page.tsx
Normal file
647
app/(main)/rapports/ca/page.tsx
Normal 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;
|
||||
887
app/(main)/rapports/clients/page.tsx
Normal file
887
app/(main)/rapports/clients/page.tsx
Normal 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;
|
||||
984
app/(main)/rapports/equipes/page.tsx
Normal file
984
app/(main)/rapports/equipes/page.tsx
Normal 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;
|
||||
703
app/(main)/rapports/page.tsx
Normal file
703
app/(main)/rapports/page.tsx
Normal 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;
|
||||
786
app/(main)/rapports/rentabilite/page.tsx
Normal file
786
app/(main)/rapports/rentabilite/page.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user