- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
480 lines
20 KiB
TypeScript
480 lines
20 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
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 { devisService } from '../../../../services/api';
|
|
import { formatCurrency, formatDate } from '../../../../utils/formatters';
|
|
|
|
interface DevisStats {
|
|
totalDevis: number;
|
|
montantTotal: number;
|
|
tauxAcceptation: number;
|
|
tauxConversion: number;
|
|
delaiMoyenReponse: number;
|
|
repartitionStatuts: { [key: string]: number };
|
|
evolutionMensuelle: Array<{ mois: string; nombre: number; montant: number }>;
|
|
topClients: Array<{ client: string; nombre: number; montant: number }>;
|
|
performanceCommerciale: Array<{ commercial: string; devis: number; acceptes: number; montant: number }>;
|
|
}
|
|
|
|
const DevisStatsPage = () => {
|
|
const toast = useRef<Toast>(null);
|
|
|
|
const [stats, setStats] = useState<DevisStats>({
|
|
totalDevis: 0,
|
|
montantTotal: 0,
|
|
tauxAcceptation: 0,
|
|
tauxConversion: 0,
|
|
delaiMoyenReponse: 0,
|
|
repartitionStatuts: {},
|
|
evolutionMensuelle: [],
|
|
topClients: [],
|
|
performanceCommerciale: []
|
|
});
|
|
|
|
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 devisService.getStatistiques(dateDebut, dateFin);
|
|
|
|
// Données simulées pour la démonstration
|
|
const mockStats: DevisStats = {
|
|
totalDevis: 156,
|
|
montantTotal: 2450000,
|
|
tauxAcceptation: 68.5,
|
|
tauxConversion: 72.3,
|
|
delaiMoyenReponse: 5.2,
|
|
repartitionStatuts: {
|
|
'ACCEPTE': 107,
|
|
'EN_ATTENTE': 23,
|
|
'REFUSE': 18,
|
|
'EXPIRE': 8
|
|
},
|
|
evolutionMensuelle: [
|
|
{ mois: 'Jan', nombre: 12, montant: 180000 },
|
|
{ mois: 'Fév', nombre: 15, montant: 220000 },
|
|
{ mois: 'Mar', nombre: 18, montant: 280000 },
|
|
{ mois: 'Avr', nombre: 14, montant: 195000 },
|
|
{ mois: 'Mai', nombre: 20, montant: 310000 },
|
|
{ mois: 'Jun', nombre: 16, montant: 240000 },
|
|
{ mois: 'Jul', nombre: 22, montant: 350000 },
|
|
{ mois: 'Aoû', nombre: 19, montant: 290000 },
|
|
{ mois: 'Sep', nombre: 20, montant: 395000 }
|
|
],
|
|
topClients: [
|
|
{ client: 'Bouygues Construction', nombre: 8, montant: 450000 },
|
|
{ client: 'Vinci Construction', nombre: 6, montant: 380000 },
|
|
{ client: 'Eiffage', nombre: 5, montant: 320000 },
|
|
{ client: 'Spie Batignolles', nombre: 4, montant: 280000 },
|
|
{ client: 'GTM Bâtiment', nombre: 3, montant: 210000 }
|
|
],
|
|
performanceCommerciale: [
|
|
{ commercial: 'Jean Dupont', devis: 45, acceptes: 32, montant: 680000 },
|
|
{ commercial: 'Marie Martin', devis: 38, acceptes: 26, montant: 520000 },
|
|
{ commercial: 'Pierre Durand', devis: 42, acceptes: 28, montant: 590000 },
|
|
{ commercial: 'Sophie Bernard', devis: 31, acceptes: 21, montant: 410000 }
|
|
]
|
|
};
|
|
|
|
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: 'Nombre de devis',
|
|
data: stats.evolutionMensuelle.map(item => item.nombre),
|
|
borderColor: '#3B82F6',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Montant (k€)',
|
|
data: stats.evolutionMensuelle.map(item => item.montant / 1000),
|
|
borderColor: '#10B981',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
yAxisID: 'y1'
|
|
}
|
|
]
|
|
};
|
|
|
|
const repartitionData = {
|
|
labels: Object.keys(stats.repartitionStatuts),
|
|
datasets: [{
|
|
data: Object.values(stats.repartitionStatuts),
|
|
backgroundColor: [
|
|
'#10B981', // ACCEPTE - vert
|
|
'#F59E0B', // EN_ATTENTE - orange
|
|
'#EF4444', // REFUSE - rouge
|
|
'#6B7280' // EXPIRE - gris
|
|
]
|
|
}]
|
|
};
|
|
|
|
const getStatutSeverity = (statut: string) => {
|
|
switch (statut) {
|
|
case 'ACCEPTE': return 'success';
|
|
case 'REFUSE': return 'danger';
|
|
case 'EXPIRE': return 'warning';
|
|
case 'EN_ATTENTE': return 'info';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const toolbarStartTemplate = () => (
|
|
<div className="flex align-items-center gap-2">
|
|
<h2 className="text-xl font-bold m-0">Statistiques des Devis</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 Devis</span>
|
|
<div className="text-900 font-medium text-xl">{stats.totalDevis}</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-edit text-blue-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">Montant Total</span>
|
|
<div className="text-900 font-medium text-xl">{formatCurrency(stats.montantTotal)}</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">+8% </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 d'Acceptation</span>
|
|
<div className="text-900 font-medium text-xl">{stats.tauxAcceptation}%</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.tauxAcceptation} 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</span>
|
|
<div className="text-900 font-medium text-xl">{stats.delaiMoyenReponse} 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">+0.3j </span>
|
|
<span className="text-500">vs période précédente</span>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphiques */}
|
|
<div className="col-12 lg:col-8">
|
|
<Card title="Évolution mensuelle">
|
|
<Chart
|
|
type="line"
|
|
data={evolutionData}
|
|
options={{
|
|
...chartOptions,
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Nombre de devis'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Montant (k€)'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
}
|
|
}
|
|
}}
|
|
height="300px"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-4">
|
|
<Card title="Répartition par statut">
|
|
<Chart
|
|
type="doughnut"
|
|
data={repartitionData}
|
|
options={chartOptions}
|
|
height="300px"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Top clients */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Top 5 Clients">
|
|
<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 Devis"
|
|
style={{ width: '100px' }}
|
|
/>
|
|
<Column
|
|
field="montant"
|
|
header="Montant"
|
|
style={{ width: '120px' }}
|
|
body={(rowData) => formatCurrency(rowData.montant)}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Performance commerciale */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Performance Commerciale">
|
|
<DataTable value={stats.performanceCommerciale} responsiveLayout="scroll">
|
|
<Column field="commercial" header="Commercial" />
|
|
<Column
|
|
field="devis"
|
|
header="Devis"
|
|
style={{ width: '80px' }}
|
|
/>
|
|
<Column
|
|
field="acceptes"
|
|
header="Acceptés"
|
|
style={{ width: '80px' }}
|
|
/>
|
|
<Column
|
|
header="Taux"
|
|
style={{ width: '80px' }}
|
|
body={(rowData) => (
|
|
<Tag
|
|
value={`${Math.round((rowData.acceptes / rowData.devis) * 100)}%`}
|
|
severity={rowData.acceptes / rowData.devis > 0.7 ? 'success' : 'warning'}
|
|
/>
|
|
)}
|
|
/>
|
|
<Column
|
|
field="montant"
|
|
header="CA"
|
|
style={{ width: '120px' }}
|
|
body={(rowData) => formatCurrency(rowData.montant)}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Analyse détaillée */}
|
|
<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>
|
|
Tendances
|
|
</h6>
|
|
<ul className="text-sm text-blue-800 list-none p-0 m-0">
|
|
<li className="mb-1">• Croissance de 12% du nombre de devis</li>
|
|
<li className="mb-1">• Augmentation de 8% du CA</li>
|
|
<li className="mb-1">• Taux d'acceptation stable à 68.5%</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">• Délai de réponse en hausse (+0.3j)</li>
|
|
<li className="mb-1">• 23 devis en attente de réponse</li>
|
|
<li className="mb-1">• 8 devis expirés ce mois</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>
|
|
Recommandations
|
|
</h6>
|
|
<ul className="text-sm text-green-800 list-none p-0 m-0">
|
|
<li className="mb-1">• Relancer les devis en attente</li>
|
|
<li className="mb-1">• Optimiser les délais de réponse</li>
|
|
<li className="mb-1">• Cibler les gros clients</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DevisStatsPage;
|