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

703 lines
27 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Chart } from 'primereact/chart';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Dropdown } from 'primereact/dropdown';
import { TabView, TabPanel } from 'primereact/tabview';
import { 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;