Initial commit
This commit is contained in:
527
app/(main)/factures/stats/page.tsx
Normal file
527
app/(main)/factures/stats/page.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Chart } from 'primereact/chart';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { factureService } from '../../../../services/api';
|
||||
import { formatCurrency, formatDate } from '../../../../utils/formatters';
|
||||
|
||||
interface FactureStats {
|
||||
totalFactures: number;
|
||||
chiffreAffaires: number;
|
||||
montantEnAttente: number;
|
||||
montantEnRetard: number;
|
||||
tauxRecouvrement: number;
|
||||
delaiMoyenPaiement: number;
|
||||
repartitionStatuts: { [key: string]: number };
|
||||
evolutionMensuelle: Array<{ mois: string; emises: number; payees: number; montantEmis: number; montantPaye: number }>;
|
||||
topClients: Array<{ client: string; nombre: number; montant: number; montantPaye: number }>;
|
||||
retardsParClient: Array<{ client: string; factures: number; montant: number; retardMoyen: number }>;
|
||||
}
|
||||
|
||||
const FactureStatsPage = () => {
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
const [stats, setStats] = useState<FactureStats>({
|
||||
totalFactures: 0,
|
||||
chiffreAffaires: 0,
|
||||
montantEnAttente: 0,
|
||||
montantEnRetard: 0,
|
||||
tauxRecouvrement: 0,
|
||||
delaiMoyenPaiement: 0,
|
||||
repartitionStatuts: {},
|
||||
evolutionMensuelle: [],
|
||||
topClients: [],
|
||||
retardsParClient: []
|
||||
});
|
||||
|
||||
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 [periodeSelectionnee, setPeriodeSelectionnee] = useState('annee');
|
||||
|
||||
const periodeOptions = [
|
||||
{ label: 'Cette année', value: 'annee' },
|
||||
{ label: 'Ce trimestre', value: 'trimestre' },
|
||||
{ label: 'Ce mois', value: 'mois' },
|
||||
{ label: 'Personnalisée', value: 'custom' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, [dateDebut, dateFin]);
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: Remplacer par un vrai appel API
|
||||
// const response = await factureService.getStatistiques(dateDebut, dateFin);
|
||||
|
||||
// Données simulées pour la démonstration
|
||||
const mockStats: FactureStats = {
|
||||
totalFactures: 234,
|
||||
chiffreAffaires: 3250000,
|
||||
montantEnAttente: 485000,
|
||||
montantEnRetard: 125000,
|
||||
tauxRecouvrement: 87.5,
|
||||
delaiMoyenPaiement: 28.5,
|
||||
repartitionStatuts: {
|
||||
'PAYEE': 156,
|
||||
'ENVOYEE': 45,
|
||||
'EN_RETARD': 18,
|
||||
'PARTIELLEMENT_PAYEE': 12,
|
||||
'BROUILLON': 3
|
||||
},
|
||||
evolutionMensuelle: [
|
||||
{ mois: 'Jan', emises: 18, payees: 15, montantEmis: 285000, montantPaye: 240000 },
|
||||
{ mois: 'Fév', emises: 22, payees: 19, montantEmis: 340000, montantPaye: 295000 },
|
||||
{ mois: 'Mar', emises: 25, payees: 22, montantEmis: 395000, montantPaye: 350000 },
|
||||
{ mois: 'Avr', emises: 20, payees: 18, montantEmis: 315000, montantPaye: 285000 },
|
||||
{ mois: 'Mai', emises: 28, payees: 24, montantEmis: 445000, montantPaye: 380000 },
|
||||
{ mois: 'Jun', emises: 24, payees: 21, montantEmis: 375000, montantPaye: 320000 },
|
||||
{ mois: 'Jul', emises: 30, payees: 26, montantEmis: 485000, montantPaye: 420000 },
|
||||
{ mois: 'Aoû', emises: 26, payees: 23, montantEmis: 410000, montantPaye: 365000 },
|
||||
{ mois: 'Sep', emises: 29, payees: 25, montantEmis: 465000, montantPaye: 395000 }
|
||||
],
|
||||
topClients: [
|
||||
{ client: 'Bouygues Construction', nombre: 12, montant: 680000, montantPaye: 620000 },
|
||||
{ client: 'Vinci Construction', nombre: 9, montant: 520000, montantPaye: 485000 },
|
||||
{ client: 'Eiffage', nombre: 8, montant: 445000, montantPaye: 445000 },
|
||||
{ client: 'Spie Batignolles', nombre: 6, montant: 385000, montantPaye: 320000 },
|
||||
{ client: 'GTM Bâtiment', nombre: 5, montant: 295000, montantPaye: 295000 }
|
||||
],
|
||||
retardsParClient: [
|
||||
{ client: 'Constructa SARL', factures: 3, montant: 85000, retardMoyen: 45 },
|
||||
{ client: 'Bâti Plus', factures: 2, montant: 65000, retardMoyen: 38 },
|
||||
{ client: 'Rénov Express', factures: 4, montant: 125000, retardMoyen: 32 },
|
||||
{ client: 'Maisons du Sud', factures: 1, montant: 35000, retardMoyen: 28 }
|
||||
]
|
||||
};
|
||||
|
||||
setStats(mockStats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des statistiques:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les statistiques'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeriodeChange = (periode: string) => {
|
||||
setPeriodeSelectionnee(periode);
|
||||
const now = new Date();
|
||||
|
||||
switch (periode) {
|
||||
case 'annee':
|
||||
setDateDebut(new Date(now.getFullYear(), 0, 1));
|
||||
setDateFin(new Date());
|
||||
break;
|
||||
case 'trimestre':
|
||||
const trimestre = Math.floor(now.getMonth() / 3);
|
||||
setDateDebut(new Date(now.getFullYear(), trimestre * 3, 1));
|
||||
setDateFin(new Date());
|
||||
break;
|
||||
case 'mois':
|
||||
setDateDebut(new Date(now.getFullYear(), now.getMonth(), 1));
|
||||
setDateFin(new Date());
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Configuration des graphiques
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom' as const
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const evolutionData = {
|
||||
labels: stats.evolutionMensuelle.map(item => item.mois),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Factures émises',
|
||||
data: stats.evolutionMensuelle.map(item => item.emises),
|
||||
borderColor: '#3B82F6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Factures payées',
|
||||
data: stats.evolutionMensuelle.map(item => item.payees),
|
||||
borderColor: '#10B981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const chiffreAffairesData = {
|
||||
labels: stats.evolutionMensuelle.map(item => item.mois),
|
||||
datasets: [
|
||||
{
|
||||
label: 'CA émis (k€)',
|
||||
data: stats.evolutionMensuelle.map(item => item.montantEmis / 1000),
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.8)'
|
||||
},
|
||||
{
|
||||
label: 'CA encaissé (k€)',
|
||||
data: stats.evolutionMensuelle.map(item => item.montantPaye / 1000),
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.8)'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const repartitionData = {
|
||||
labels: Object.keys(stats.repartitionStatuts),
|
||||
datasets: [{
|
||||
data: Object.values(stats.repartitionStatuts),
|
||||
backgroundColor: [
|
||||
'#10B981', // PAYEE - vert
|
||||
'#3B82F6', // ENVOYEE - bleu
|
||||
'#EF4444', // EN_RETARD - rouge
|
||||
'#F59E0B', // PARTIELLEMENT_PAYEE - orange
|
||||
'#6B7280' // BROUILLON - gris
|
||||
]
|
||||
}]
|
||||
};
|
||||
|
||||
const getStatutSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'PAYEE': return 'success';
|
||||
case 'EN_RETARD': return 'danger';
|
||||
case 'PARTIELLEMENT_PAYEE': return 'warning';
|
||||
case 'ENVOYEE': return 'info';
|
||||
case 'BROUILLON': return 'secondary';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const toolbarStartTemplate = () => (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<h2 className="text-xl font-bold m-0">Statistiques des Factures</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
const toolbarEndTemplate = () => (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Dropdown
|
||||
value={periodeSelectionnee}
|
||||
options={periodeOptions}
|
||||
onChange={(e) => handlePeriodeChange(e.value)}
|
||||
className="w-10rem"
|
||||
/>
|
||||
{periodeSelectionnee === 'custom' && (
|
||||
<>
|
||||
<Calendar
|
||||
value={dateDebut}
|
||||
onChange={(e) => setDateDebut(e.value || new Date())}
|
||||
placeholder="Date début"
|
||||
dateFormat="dd/mm/yy"
|
||||
/>
|
||||
<Calendar
|
||||
value={dateFin}
|
||||
onChange={(e) => setDateFin(e.value || new Date())}
|
||||
placeholder="Date fin"
|
||||
dateFormat="dd/mm/yy"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadStats}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<div className="col-12">
|
||||
<Toolbar start={toolbarStartTemplate} end={toolbarEndTemplate} />
|
||||
</div>
|
||||
|
||||
{/* KPIs principaux */}
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Total Factures</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.totalFactures}</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-file text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-green-500 font-medium">+15% </span>
|
||||
<span className="text-500">vs période précédente</span>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Chiffre d'Affaires</span>
|
||||
<div className="text-900 font-medium text-xl">{formatCurrency(stats.chiffreAffaires)}</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-euro text-green-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-green-500 font-medium">+12% </span>
|
||||
<span className="text-500">vs période précédente</span>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Taux de Recouvrement</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.tauxRecouvrement}%</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-orange-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-chart-line text-orange-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar value={stats.tauxRecouvrement} className="mt-2" style={{ height: '6px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Délai Moyen Paiement</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.delaiMoyenPaiement} jours</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-cyan-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-clock text-cyan-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-red-500 font-medium">+2.5j </span>
|
||||
<span className="text-500">vs période précédente</span>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alertes financières */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h5 className="m-0">Alertes Financières</h5>
|
||||
<Badge value="2" severity="danger" />
|
||||
</div>
|
||||
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<div className="p-3 border-round bg-red-50 border-left-3 border-red-500">
|
||||
<div className="flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 className="text-red-900 mb-1">Factures en retard</h6>
|
||||
<p className="text-red-800 text-sm m-0">{formatCurrency(stats.montantEnRetard)} à recouvrer</p>
|
||||
</div>
|
||||
<Badge value={stats.repartitionStatuts['EN_RETARD'] || 0} severity="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="p-3 border-round bg-orange-50 border-left-3 border-orange-500">
|
||||
<div className="flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 className="text-orange-900 mb-1">En attente de paiement</h6>
|
||||
<p className="text-orange-800 text-sm m-0">{formatCurrency(stats.montantEnAttente)} en cours</p>
|
||||
</div>
|
||||
<Badge value={stats.repartitionStatuts['ENVOYEE'] || 0} severity="warning" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Répartition par statut" className="h-full">
|
||||
<Chart
|
||||
type="doughnut"
|
||||
data={repartitionData}
|
||||
options={chartOptions}
|
||||
height="250px"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Graphiques d'évolution */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Évolution du nombre de factures">
|
||||
<Chart
|
||||
type="line"
|
||||
data={evolutionData}
|
||||
options={chartOptions}
|
||||
height="300px"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Évolution du chiffre d'affaires">
|
||||
<Chart
|
||||
type="bar"
|
||||
data={chiffreAffairesData}
|
||||
options={chartOptions}
|
||||
height="300px"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Top clients */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Top 5 Clients par CA">
|
||||
<DataTable value={stats.topClients} responsiveLayout="scroll">
|
||||
<Column
|
||||
field="client"
|
||||
header="Client"
|
||||
body={(rowData, options) => (
|
||||
<div className="flex align-items-center">
|
||||
<Badge value={options.rowIndex + 1} className="mr-2" />
|
||||
{rowData.client}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Column
|
||||
field="nombre"
|
||||
header="Nb Factures"
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
<Column
|
||||
field="montant"
|
||||
header="CA Total"
|
||||
style={{ width: '120px' }}
|
||||
body={(rowData) => formatCurrency(rowData.montant)}
|
||||
/>
|
||||
<Column
|
||||
header="Taux Paiement"
|
||||
style={{ width: '120px' }}
|
||||
body={(rowData) => (
|
||||
<Tag
|
||||
value={`${Math.round((rowData.montantPaye / rowData.montant) * 100)}%`}
|
||||
severity={rowData.montantPaye / rowData.montant > 0.9 ? 'success' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Clients en retard */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Clients avec retards de paiement">
|
||||
<DataTable value={stats.retardsParClient} responsiveLayout="scroll">
|
||||
<Column field="client" header="Client" />
|
||||
<Column
|
||||
field="factures"
|
||||
header="Nb Factures"
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
<Column
|
||||
field="montant"
|
||||
header="Montant"
|
||||
style={{ width: '120px' }}
|
||||
body={(rowData) => formatCurrency(rowData.montant)}
|
||||
/>
|
||||
<Column
|
||||
field="retardMoyen"
|
||||
header="Retard Moyen"
|
||||
style={{ width: '120px' }}
|
||||
body={(rowData) => (
|
||||
<Tag
|
||||
value={`${rowData.retardMoyen}j`}
|
||||
severity={rowData.retardMoyen > 30 ? 'danger' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Analyse et recommandations */}
|
||||
<div className="col-12">
|
||||
<Card title="Analyse et Recommandations">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="p-3 border-round bg-blue-50">
|
||||
<h6 className="text-blue-900 mb-2">
|
||||
<i className="pi pi-chart-line mr-2"></i>
|
||||
Performance
|
||||
</h6>
|
||||
<ul className="text-sm text-blue-800 list-none p-0 m-0">
|
||||
<li className="mb-1">• CA en croissance de 12%</li>
|
||||
<li className="mb-1">• Taux de recouvrement à 87.5%</li>
|
||||
<li className="mb-1">• 234 factures émises</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="p-3 border-round bg-orange-50">
|
||||
<h6 className="text-orange-900 mb-2">
|
||||
<i className="pi pi-exclamation-triangle mr-2"></i>
|
||||
Points d'attention
|
||||
</h6>
|
||||
<ul className="text-sm text-orange-800 list-none p-0 m-0">
|
||||
<li className="mb-1">• {formatCurrency(stats.montantEnRetard)} en retard</li>
|
||||
<li className="mb-1">• Délai moyen en hausse (+2.5j)</li>
|
||||
<li className="mb-1">• 18 factures en retard</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="p-3 border-round bg-green-50">
|
||||
<h6 className="text-green-900 mb-2">
|
||||
<i className="pi pi-lightbulb mr-2"></i>
|
||||
Actions recommandées
|
||||
</h6>
|
||||
<ul className="text-sm text-green-800 list-none p-0 m-0">
|
||||
<li className="mb-1">• Relancer les impayés</li>
|
||||
<li className="mb-1">• Revoir les conditions de paiement</li>
|
||||
<li className="mb-1">• Mettre en place des acomptes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FactureStatsPage;
|
||||
Reference in New Issue
Block a user