- 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>
890 lines
41 KiB
TypeScript
890 lines
41 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
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: string = 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: string = 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: string = 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)}
|
|
selectionMode="checkbox"
|
|
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; |